404 Not Found

404 Not Found


nginx

「SQLite と Prisma ORM:型安全なデータベース操作」

チャーリーは、ローカルデータストレージを必要とするデスクトップ用メモアプリを開発中ですが、ユーザーに別途MySQLサービスをインストールさせるのは避けたいと考えています。SQLiteは、単一のファイルがデータベース全体を構成する、設定不要の組み込みデータベースです。Prisma ORMと組み合わせることで、SQL操作に対して完全な型安全性を提供します。

1. better-sqlite3:同期型APIを備えた組み込みデータベース

(1) なぜbetter-sqlite3を選ぶのか?

Node.jsのエコシステムにはいくつかのSQLiteライブラリが存在しますが、better-sqlite3はその同期APIで知られています。このライブラリは、awaitやネストされたコールバックを必要とせず、呼び出し直後に即座に結果を返します。低レベルでSQLiteをC++でコンパイルするため、非同期のラッパーライブラリをはるかに上回るパフォーマンスを発揮します。

機能 説明
Sync API コールバック地獄なし、直感的で読みやすいコード
ゼロ設定 データベースサービスのインストールは不要—npm installはすぐに使用可能です
単一ファイル形式 データベース全体が単一の .db ファイルで構成される
トランザクションのサポート ネストされたトランザクションやプリペアードステートメント――すべて含まれています
高性能 C++ バインディング、node-sqlite3 より 2~3 倍高速
クロスプラットフォーム Windows、macOS、Linux でコンパイル可能

(2) 設置と基本的な接続

BASH
npm init -y
npm install better-sqlite3
JAVASCRIPT
const Database = require('better-sqlite3');
const db = new Database('myapp.db');

db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

console.log('SQLite Version:', db.prepare('SELECT sqlite_version()').get());

(3) 一般的な手法のクイックリファレンスガイド

方法 目的
db.prepare(sql) プリコンパイル済みステートメントを作成する const stmt = db.prepare('SELECT * FROM users WHERE id = ?')
stmt.run(...params) INSERT/UPDATE/DELETE を実行 stmt.run(1, 'Charlie')
stmt.get(...params) 1行のオブジェクトに戻る stmt.get(1)
stmt.all(...params) すべての行の配列を返す stmt.all()
stmt.values(...params) 戻り値の配列(キー名なし) stmt.values()
db.exec(sql) 複数のSQL文を実行する db.exec(schemaSql)
db.transaction(fn) トランザクション関数の作成 const insertMany = db.transaction((items) => {...})
db.pragma(cmd) PRAGMA の設定/照会 db.pragma('journal_mode = WAL')

▶ 例:better-sqlite3 を使った完全な CRUD ワークフロー

JAVASCRIPT
const Database = require('better-sqlite3');
const db = new Database('notes.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    body  TEXT DEFAULT '',
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

const insert = db.prepare('INSERT INTO notes (title, body) VALUES (?, ?)');
const result = insert.run('First Note', 'Hello SQLite!');
console.log('Insert a row ID:', result.lastInsertRowid);

const find = db.prepare('SELECT * FROM notes WHERE id = ?');
console.log('Search Results:', find.get(1));

const update = db.prepare('UPDATE notes SET title = ? WHERE id = ?');
update.run('Revised Title', 1);

const remove = db.prepare('DELETE FROM notes WHERE id = ?');
remove.run(1);

const listAll = db.prepare('SELECT * FROM notes ORDER BY created_at DESC');
console.log('All Notes:', listAll.all());

db.close();

(4) トランザクションでの使用

JAVASCRIPT
const insertMany = db.transaction((notes) => {
  for (const n of notes) {
    insert.run(n.title, n.body);
  }
});

insertMany([
  { title: 'Notes A', body: 'Content A' },
  { title: 'Notes B', body: 'Content B' },
]);

2. SQL文の基礎の復習

(1) 4つの主要な操作:CRUD

操作 SQL キーワード
作成 INSERT INSERT INTO table (col) VALUES (val)
読む 選択 SELECT col FROM table WHERE cond
更新 UPDATE UPDATE table SET col=val WHERE cond
削除 DELETE DELETE FROM table WHERE cond

(2) 一般的なクエリ句

TEXT
SELECT Listed
FROM Table Name
WHERE Conditions
GROUP BY Grouping Column
HAVING Grouping Criteria
ORDER BY Sorted List ASC|DESC
LIMIT Quantity OFFSET offset

(3) クエリの結合

JAVASCRIPT
db.exec(`
  CREATE TABLE IF NOT EXISTS authors (
    id   INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL
  )
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS books (
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    title     TEXT NOT NULL,
    author_id INTEGER REFERENCES authors(id)
  )
`);

const joinQuery = db.prepare(`
  SELECT books.title, authors.name AS author
  FROM books
  JOIN authors ON books.author_id = authors.id
`);

3. Prismaのインストールと初期設定

(1) Prismaとは何ですか?

Prismaは、次世代のNode.js/TypeScript用ORMです。その基本的なワークフローは以下の通りです:

100%
flowchart LR
    A["schema.prisma"] -->|"prisma migrate dev"| B["Migration SQL"]
    A -->|"prisma generate"| C["Prisma Client"]
    C -->|"Type-Safe Queries"| D[("Database")]
    B --> D

(2) プロジェクトの立ち上げ

BASH
mkdir prisma-notes && cd prisma-notes
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider sqlite

初期化後に生成されます:

TEXT
prisma-notes/
├── prisma/
│   └── schema.prisma
├── .env
└── package.json

.env ファイルの内容:

TEXT
DATABASE_URL="file:./dev.db"

(3) Prisma Command クイックリファレンス

コマンド 目的
npx prisma init Prismaプロジェクトの初期化
npx prisma migrate dev 開発用移行の作成と適用
npx prisma migrate deploy 本番環境へのアプリケーション移行
npx prisma generate Prismaクライアントを生成
npx prisma studio ビジュアル管理インターフェースを開く
npx prisma db push プロトタイプ段階でスキーマを直接反映する(マイグレーションファイルを生成せずに)
npx prisma db seed シードデータスクリプトを実行

4. schema.prisma:モデルの定義

(1) 基本構造

JAVASCRIPT
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Note {
  id        Int      @id @default(autoincrement())
  title     String
  body      String   @default("")
  pinned    Boolean  @default(false)
  tags      String   @default("")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

(2) フィールド型のクイックリファレンス

プリズマ・タイプ SQLite マッピング 説明
String TEXT 文字列
Int INTEGER 32ビット整数
BigInt INTEGER 64ビット整数
Float REAL 浮動小数点数
Boolean 整数 0 または 1
DateTime テキスト ISO 8601 文字列
Json TEXT JSON文字列
Bytes BLOB バイナリデータ
Decimal テキスト 高精度小数

注:SQLiteの型システムはPostgreSQLのものとは異なります。Prismaが内部的な変換を処理します。providerに切り替えると、フィールド型のマッピングが自動的に調整されます。

(3) プロパティと修飾子

修飾子 目的
@id 主キー id Int @id
@default デフォルト値 @default(autoincrement()) / @default(now()) / @default("active")
@unique 一意性制約 email String @unique
@relation 関係の定義 @relation(fields: [authorId], references: [id])
@map / @@map 列/テーブル名の対応 @map("created_at")
@@unique 複合一意 @@unique([firstName, lastName])
@@index 総合指数 @@index([categoryId, createdAt])
? 任意のフィールド bio String?

(4) 関係の定義

JAVASCRIPT
model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
  notes Note[]
}

model Note {
  id       Int   @id @default(autoincrement())
  title    String
  body     String @default("")
  authorId Int
  author   User   @relation(fields: [authorId], references: [id], onDelete: Cascade)
}

5. Prisma Migrate による移行

(1) 移行を作成して適用する

BASH
npx prisma migrate dev --name init

実行後:

TEXT
prisma/
├── schema.prisma
└── migrations/
    └── 20260703_init/
        └── migration.sql

生成済み migration.sql

SQL
CREATE TABLE "Note" (
    "id"        INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title"     TEXT NOT NULL,
    "body"      TEXT NOT NULL DEFAULT '',
    "pinned"    BOOLEAN NOT NULL DEFAULT false,
    "tags"      TEXT NOT NULL DEFAULT '',
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" DATETIME NOT NULL
);

(2) 移行ワークフロー

TEXT
Development Phase:  schema Edit → npx prisma migrate dev --name Description
Testing Phase:  npx prisma migrate deploy(App Only,Do not create a new migration)
Prototyping Phase:  npx prisma db push(Skip migrating files,Rapid Iteration)
Reset Data:  npx prisma migrate reset(Clear the database and replay all migrations)

(3) 本番環境へのデプロイ

BASH
npx prisma migrate deploy

migrate deploy 適用されていないマイグレーションのみを実行します。新しいマイグレーションを作成したり、データをリセットしたりすることはありません。CI/CDパイプラインに最適です。


6. Prisma Client の CRUD

(1) クライアントを初期化する

JAVASCRIPT
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function main() {
  // CRUD Instructions are written here
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

(2) 作成 — 作成

JAVASCRIPT
const note = await prisma.note.create({
  data: {
    title: 'Study Prisma',
    body: 'Prisma Making Database Operations Type-Safe',
    pinned: true,
  },
});

const notes = await prisma.note.createMany({
  data: [
    { title: 'Notes A', body: 'Content A' },
    { title: 'Notes B', body: 'Content B', pinned: true },
  ],
});

(3) 読み取り — クエリ

JAVASCRIPT
const one = await prisma.note.findUnique({ where: { id: 1 } });

const first = await prisma.note.findFirst({
  where: { pinned: true },
  orderBy: { createdAt: 'desc' },
});

const all = await prisma.note.findMany();

const filtered = await prisma.note.findMany({
  where: {
    pinned: true,
    title: { contains: 'Prisma' },
  },
});

(4) 更新 — 更新

JAVASCRIPT
const updated = await prisma.note.update({
  where: { id: 1 },
  data: { title: 'Updated Title', pinned: false },
});

const count = await prisma.note.updateMany({
  where: { pinned: false },
  data: { tags: 'archived' },
});

(5) 削除 — 削除

JAVASCRIPT
const deleted = await prisma.note.delete({ where: { id: 1 } });

const deleteCount = await prisma.note.deleteMany({
  where: { pinned: false },
});

(6) クエリフィルターの一覧

フィルター 意味
equals { title: { equals: 'Hello' } }
not { id: { not: 1 } }
contains 含める { title: { contains: 'Prisma' } }
startsWith プレフィックス { title: { startsWith: 'Learn' } }
endsWith 接尾辞 { email: { endsWith: '@test.com' } }
in リスト内 { id: { in: [1, 2, 3] } }
notIn リストにありません { id: { notIn: [4, 5] } }
lt / lte 未満 / 以下 { id: { lte: 10 } }
gt / gte より大きい / 以上 { id: { gte: 5 } }
AND そして { AND: [{ pinned: true }, { title: { contains: 'A' } }] }
OR または { OR: [{ pinned: true }, { pinned: false }] }
NOT いいえ { NOT: { title: 'Hello' } }

7. 並べ替えとページネーション

(1) 並べ替え

JAVASCRIPT
const sorted = await prisma.note.findMany({
  orderBy: [
    { pinned: 'desc' },
    { createdAt: 'desc' },
  ],
});

(2) ページ番号付け

JAVASCRIPT
const PAGE_SIZE = 10;

const page1 = await prisma.note.findMany({
  skip: 0,
  take: PAGE_SIZE,
  orderBy: { createdAt: 'desc' },
});

const page2 = await prisma.note.findMany({
  skip: PAGE_SIZE,
  take: PAGE_SIZE,
  orderBy: { createdAt: 'desc' },
});

(3) カーソルによるページネーション(大規模なデータセットに推奨)

JAVASCRIPT
const first = await prisma.note.findMany({
  take: 10,
  orderBy: { id: 'asc' },
});

const cursor = first[first.length - 1].id;

const next = await prisma.note.findMany({
  take: 10,
  skip: 1,
  cursor: { id: cursor },
  orderBy: { id: 'asc' },
});

8. データベースの選定比較

(1) SQLite 対 MySQL 対 PostgreSQL 対 MongoDB

ディメンション SQLite MySQL PostgreSQL MongoDB
タイプ 組み込み型 クライアント・サーバー型 クライアント・サーバー型 ドキュメントベース型
インストール インストール不要(npm パッケージに含まれている) サービスのインストールが必要 サービスのインストールが必要 サービスのインストールが必要
並行書き込み 書き込み元が1つの場合 書き込み元が複数ある場合 書き込み元が複数ある場合 書き込み元が複数ある場合
データサイズ 小~中規模(GB単位) 大規模(TB単位) 大規模(TB単位) 大規模(TB単位)
ユースケース デスクトップアプリケーション/プロトタイプ/テスト Webアプリケーション/中規模プロジェクト 複雑なクエリ/地理空間データ 柔軟なスキーマ/ログ
JSONのサポート 限定的(JSON-1拡張機能) サポート済み ネイティブJSONB ネイティブのドキュメント
全文検索 FTS5 拡張機能 全文インデックス tsvector テキストインデックス
トランザクション 完全なACID 完全なACID 完全なACID 4.0以降のマルチドキュメント・トランザクション
ライセンス パブリックドメイン GPL / 商用 PostgreSQL SSPL

(2) ORMフレームワークの比較

ディメンション Mongoose Prisma Sequelize TypeORM
言語 JavaScript TypeScript 推奨 JavaScript TypeScript 推奨
データベース MongoDBのみ SQLite/MySQL/PostgreSQL/MongoDB MySQL/PostgreSQL/SQLite/MSSQL MySQL/PostgreSQL/SQLite/MSSQL
スキーマ定義 JSオブジェクト .prisma 宣言型ファイル JSモデル定義 デコレータ/エンティティクラス
型安全性 弱い(手動) 強い(自動生成) 弱い 中程度(デコレータ型)
移行ツール 組み込みではない prisma migrate sequelize-cli 組み込み
クエリ方法 チェーン型API チェーン型オブジェクト チェーン型/ネイティブSQL QueryBuilder/ネイティブ
N+1問題 データの投入が必要 自動インクルード イージー/レイジー処理が必要 リレーションが必要
コミュニティの規模 大規模 急成長中 大規模 大規模
適したプロジェクト MongoDB プロジェクト フルスタック TypeScript 従来の Node.js NestJS エコシステム

9. 包括的な例:ノート管理データ層

PrismaとSQLiteを使用して、メモ管理のための完全なCRUDデータ層を構築する。

▶ 例:Prisma + SQLite によるメモ管理

ステップ 1 — スキーマの定義

JAVASCRIPT
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Note {
  id        Int      @id @default(autoincrement())
  title     String
  body      String   @default("")
  pinned    Boolean  @default(false)
  tags      String   @default("")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt      @map("updated_at")

  @@map("notes")
}

ステップ 2 — 移行

BASH
npx prisma migrate dev --name notes_init

ステップ3 — データアクセス層

JAVASCRIPT
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function createNote(data) {
  return prisma.note.create({ data });
}

async function getNoteById(id) {
  return prisma.note.findUnique({ where: { id } });
}

async function updateNote(id, data) {
  return prisma.note.update({ where: { id }, data });
}

async function deleteNote(id) {
  return prisma.note.delete({ where: { id } });
}

async function listNotes({ page = 1, pageSize = 10, pinned, keyword } = {}) {
  const where = {};
  if (pinned !== undefined) where.pinned = pinned;
  if (keyword) where.title = { contains: keyword };

  const [items, total] = await Promise.all([
    prisma.note.findMany({
      where,
      orderBy: [{ pinned: 'desc' }, { createdAt: 'desc' }],
      skip: (page - 1) * pageSize,
      take: pageSize,
    }),
    prisma.note.count({ where }),
  ]);

  return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
}

async function togglePin(id) {
  const note = await prisma.note.findUnique({ where: { id } });
  if (!note) throw new Error('The note does not exist.');
  return prisma.note.update({
    where: { id },
    data: { pinned: !note.pinned },
  });
}

module.exports = {
  createNote,
  getNoteById,
  updateNote,
  deleteNote,
  listNotes,
  togglePin,
};

ステップ4 — 使用例

JAVASCRIPT
const db = require('./note-service');

async function main() {
  const n1 = await db.createNote({ title: 'Study Prisma', body: 'Type Safety ORM', pinned: true });
  const n2 = await db.createNote({ title: 'SQLite Key Points', body: 'Zero-Configuration Embedded Database' });
  const n3 = await db.createNote({ title: 'Prisma Migration', body: 'migrate dev Driver' });

  console.log('Single-Record Query:', await db.getNoteById(n1.id));

  await db.updateNote(n2.id, { body: 'What's New' });
  await db.togglePin(n3.id);

  const result = await db.listNotes({ page: 1, pageSize: 10, keyword: 'Prisma' });
  console.log('Search Results:', result);

  await db.deleteNote(n2.id);

  const all = await db.listNotes({ page: 1, pageSize: 10 });
  console.log('Remaining Notes:', all);
}

main()
  .catch(console.error)
  .finally(() => require('@prisma/client').PrismaClient &&
    require('./node_modules/.prisma/client').$disconnect?.());

実行:

BASH
node index.js

ステップ5 — 視覚的管理

BASH
npx prisma studio

ブラウザで http://localhost:5555 を開くと、データを視覚的に確認・編集できます。


10. このレッスンのまとめ


❓ よくある質問

Q PrismaとSequelizeの違いは何ですか?
A Prismaは宣言型スキーマを使用して型安全なクライアントを生成するのに対し、SequelizeはデコレータやdefineModelを使用します。Prismaはより優れた型安全性を提供しますが、Sequelizeはより成熟したエコシステムを備えています。
Q SQLiteは本番環境に適していますか?
A 個人プロジェクト、社内ツール、組み込みアプリケーションなど、低同時アクセス環境(書き込みよりも読み込みが多く、単一サーバーでの展開)には適しています。高同時アクセスの書き込み操作を行う場合は、PostgreSQLやMySQLを検討してください。
Q Prismaのmigratedb pushの違いは何ですか?
A migrateは、チームでの共同作業や本番環境へのデプロイに適したマイグレーションファイルを生成します。一方、db pushはスキーマをデータベースに直接同期するため、プロトタイピングに適しています。
Q PostgreSQLに切り替えるにはどうすればよいですか?
A schema.prisma のプロバイダを postgresql に変更し、DATABASE_URL の接続文字列を更新してから、prisma migrate reset を実行してデータベースを再構築してください。
Q PrismaでN+1問題をどのように解決しますか?
A INCLUDE を使用して関連データを事前に読み込むか、SELECT を使用してフィールドを正確に選択し、ループ内で関連レコードを1つずつクエリするのを避けてください。

📖 まとめ

📝 練習問題

  1. better-sqlite3 を使用して users テーブルを作成し、挿入、メールアドレスによる検索、更新、削除の各操作を実装し、トランザクションを使用して原子性を確保してください。
  2. Prismaプロジェクトを初期化し、2つのモデル——UserPost(1対多の関係)——を定義します。その後、マイグレーションを実行し、Prisma Clientを使用してユーザーの作成、記事の公開、およびユーザーとそのすべての記事のクエリを実行します。
  3. メモ管理の例を基に、Category モデルを追加して、カテゴリによるメモのフィルタリングを可能にし、listNotes ではカテゴリ ID によるフィルタリングに対応させる。
  4. better-sqlite3のネイティブSQLとPrisma Clientを使用して、同じ一連のCRUD操作を実装し、行数や可読性の違いを比較してください。
  5. prisma.note.findMany を使用して、キーワード検索、上位結果の優先表示、ページネーションを実装するスクリプトを作成し、10,000件のレコードを処理する際のオフセットページネーションとカーソルページネーションのパフォーマンスの違いを比較してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%