「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) 設置と基本的な接続
npm init -y
npm install better-sqlite3
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 ワークフロー
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) トランザクションでの使用
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) 一般的なクエリ句
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) クエリの結合
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です。その基本的なワークフローは以下の通りです:
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
- schema.prisma: データモデルの宣言型定義
- Prisma Migrate:マイグレーション用SQLを自動的に生成・実行します
- Prisma Client:自動生成される、型安全なクエリクライアント
(2) プロジェクトの立ち上げ
mkdir prisma-notes && cd prisma-notes
npm init -y
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider sqlite
初期化後に生成されます:
prisma-notes/
├── prisma/
│ └── schema.prisma
├── .env
└── package.json
.env ファイルの内容:
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) 基本構造
// 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) 関係の定義
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) 移行を作成して適用する
npx prisma migrate dev --name init
実行後:
prisma/
├── schema.prisma
└── migrations/
└── 20260703_init/
└── migration.sql
生成済み migration.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) 移行ワークフロー
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) 本番環境へのデプロイ
npx prisma migrate deploy
migrate deploy適用されていないマイグレーションのみを実行します。新しいマイグレーションを作成したり、データをリセットしたりすることはありません。CI/CDパイプラインに最適です。
6. Prisma Client の CRUD
(1) クライアントを初期化する
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) 作成 — 作成
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) 読み取り — クエリ
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) 更新 — 更新
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) 削除 — 削除
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) 並べ替え
const sorted = await prisma.note.findMany({
orderBy: [
{ pinned: 'desc' },
{ createdAt: 'desc' },
],
});
(2) ページ番号付け
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) カーソルによるページネーション(大規模なデータセットに推奨)
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 — スキーマの定義
// 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 — 移行
npx prisma migrate dev --name notes_init
ステップ3 — データアクセス層
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 — 使用例
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?.());
実行:
node index.js
ステップ5 — 視覚的管理
npx prisma studio
ブラウザで http://localhost:5555 を開くと、データを視覚的に確認・編集できます。
10. このレッスンのまとめ
- SQLite は設定不要の組み込みデータベースであり、
better-sqlite3は高性能な同期 API を提供しています - Prisma は
schema.prismaを使用してモデルを宣言的に定義し、マイグレーションと型安全なクライアントを自動的に生成します prisma migrate dev:ドライバーの開発および移行用;prisma migrate deploy:本番環境へのデプロイ用- Prisma Client は、
findMany、create、update、deleteに対応しており、フィルタリング、ソート、ページネーションに関する幅広いオプションを備えています。 - データベースの選択:デスクトップアプリやプロトタイプアプリにはSQLiteを、WebアプリにはMySQLまたはPostgreSQLを、柔軟なスキーマが必要な場合はMongoDBを選択してください
- ORMの選び方:MongoDBにはMongooseを、フルスタックTypeScriptにはPrismaを、NestJSにはTypeORMを使用する
❓ よくある質問
defineModelを使用します。Prismaはより優れた型安全性を提供しますが、Sequelizeはより成熟したエコシステムを備えています。migrateとdb pushの違いは何ですか?migrateは、チームでの共同作業や本番環境へのデプロイに適したマイグレーションファイルを生成します。一方、db pushはスキーマをデータベースに直接同期するため、プロトタイピングに適しています。schema.prisma のプロバイダを postgresql に変更し、DATABASE_URL の接続文字列を更新してから、prisma migrate reset を実行してデータベースを再構築してください。INCLUDE を使用して関連データを事前に読み込むか、SELECT を使用してフィールドを正確に選択し、ループ内で関連レコードを1つずつクエリするのを避けてください。- Q: SQLiteは並行処理に対応していますか? A: 複数の読み取りと1つの書き込みに対応しています。WALモードでは、読み取りは並行して実行できますが、書き込みは依然として順次処理されます。書き込みの並行処理が頻繁に行われるシナリオでは、MySQLまたはPostgreSQLに切り替えることをお勧めします。
- Q: PrismaとSequelize、どちらが優れていますか? A: Prismaは、より強力な型安全性、優れた移行体験、N+1問題のないクエリを提供します。一方、Sequelizeはより成熟したエコシステムを持ち、より多くのデータベース方言をサポートしています。新規プロジェクトにはPrismaをお勧めします。
- Q: なぜbetter-sqlite3は同期処理を採用しているのですか? A: SQLite自体はインプロセスライブラリであり、I/O操作にかかる時間はわずかマイクロ秒単位です。非同期操作を採用すると、かえってイベントループのオーバーヘッドが増加してしまいます。同期APIを採用することで、ネストしたコールバックが不要になり、よりすっきりとしたコードになります。
- Q: SQLite データベースの最大サイズはどれくらいですか? A: 理論上の限界は 281 TB ですが、実際にはオペレーティングシステムの単一ファイルサイズ制限に左右されます。一般的な使用状況では、数十 GB 規模のデータベースでも全く問題ありません。
- Q: いつSQLiteからPostgreSQLへ移行すべきですか? A: 複数のプロセス間での同時書き込み、ネイティブのJSONBクエリ、全文検索、地理空間データ、行レベルのセキュリティポリシーが必要になった場合、または単一マシンのストレージ容量を超過した場合には、移行すべきです。
- Q: PrismaはMongoDBに対応していますか? A: バージョン2.0以降ではMongoDBに対応していますが、リレーショナルデータベース向けの機能セットほど包括的ではありません。複雑な集計処理を行う場合は、Mongooseの使用をお勧めします。
- Q: 本番環境では、Prisma Clientのインスタンスをどのように管理すべきですか? A: リクエストごとに新しいインスタンスが作成されないよう、グローバルシングルトンを使用してください。Next.jsなどのホットリロード環境では、接続のリークを防ぐために
globalThisのキャッシュ機能を使用してください。
📖 まとめ
- 1 better-sqlite3:組み込みデータベース向け同期APIの基本概念と使用方法
- 2 SQL文の主要な概念と使い方:基礎の復習
- Prismaのインストールと初期設定に関する3つの重要な概念と使い方
- 4 schema.prisma:モデルの基本概念と使用方法の定義
- Prisma Migrateの5つの基本概念と使い方
- Prisma ClientのCRUDに関する6つの基本概念と使い方
- ソートとページネーションの7つの基本概念と使い方
- データベースの比較・選定における8つの重要な概念と手法
📝 練習問題
better-sqlite3を使用してusersテーブルを作成し、挿入、メールアドレスによる検索、更新、削除の各操作を実装し、トランザクションを使用して原子性を確保してください。- Prismaプロジェクトを初期化し、2つのモデル——
UserとPost(1対多の関係)——を定義します。その後、マイグレーションを実行し、Prisma Clientを使用してユーザーの作成、記事の公開、およびユーザーとそのすべての記事のクエリを実行します。 - メモ管理の例を基に、
Categoryモデルを追加して、カテゴリによるメモのフィルタリングを可能にし、listNotesではカテゴリ ID によるフィルタリングに対応させる。 better-sqlite3のネイティブSQLとPrisma Clientを使用して、同じ一連のCRUD操作を実装し、行数や可読性の違いを比較してください。prisma.note.findManyを使用して、キーワード検索、上位結果の優先表示、ページネーションを実装するスクリプトを作成し、10,000件のレコードを処理する際のオフセットページネーションとカーソルページネーションのパフォーマンスの違いを比較してください。



