drizzle-orm の使い方 — TypeScriptファーストなSQL ORM
一言でいうと
Drizzle ORM は、TypeScript ファーストで設計された軽量な SQL ORM です。SQL に近い直感的なクエリビルダーと、完全な型安全性を両立しながら、PostgreSQL・MySQL・SQLite・SingleStore など主要なデータベースをサポートします。
どんな時に使う?
- TypeScript プロジェクトで型安全にDBアクセスしたい時 — スキーマ定義からクエリ結果まで、すべてが型推論されるため、ランタイムエラーを大幅に削減できます。
- SQL の知識を活かしたい時 — Prisma のような独自クエリ言語ではなく、SQL に近い構文でクエリを組み立てられるため、SQL に慣れたエンジニアの学習コストが低いです。
- エッジランタイムやサーバーレス環境で使いたい時 — バンドルサイズが小さく、Cloudflare Workers(D1)、Vercel Edge Functions、Bun などの環境でも動作します。
インストール
Drizzle ORM 本体に加え、使用するデータベースに対応するドライバーパッケージが必要です。
PostgreSQL(node-postgres)の場合
# npm
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
# yarn
yarn add drizzle-orm pg
yarn add -D drizzle-kit @types/pg
# pnpm
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit @types/pg
MySQL(mysql2)の場合
npm install drizzle-orm mysql2
npm install -D drizzle-kit
SQLite(better-sqlite3)の場合
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
補足:
drizzle-kitはマイグレーション生成・スキーマ管理用の CLI ツールです。開発時に必要になるため、合わせてインストールすることを推奨します。
基本的な使い方
ここでは PostgreSQL + node-postgres を例に、スキーマ定義 → DB接続 → CRUD の一連の流れを示します。
1. スキーマ定義(src/db/schema.ts)
import { pgTable, serial, text, integer, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age'),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id')
.references(() => users.id)
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
2. DB接続(src/db/index.ts)
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool, { schema });
3. CRUD 操作
import { eq } from 'drizzle-orm';
import { db } from './db';
import { users, posts } from './db/schema';
// CREATE
const newUser = await db.insert(users).values({
name: '田中太郎',
email: 'tanaka@example.com',
age: 30,
}).returning();
// READ
const allUsers = await db.select().from(users);
// READ(条件付き)
const user = await db
.select()
.from(users)
.where(eq(users.email, 'tanaka@example.com'));
// UPDATE
await db
.update(users)
.set({ name: '田中次郎', age: 31 })
.where(eq(users.id, 1));
// DELETE
await db.delete(users).where(eq(users.id, 1));
4. drizzle-kit 設定(drizzle.config.ts)
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
# マイグレーションファイル生成
npx drizzle-kit generate
# マイグレーション実行
npx drizzle-kit migrate
# Drizzle Studio(GUI)起動
npx drizzle-kit studio
よく使う API
1. select + フィルタリング演算子
import { eq, ne, gt, gte, lt, lte, like, ilike, and, or, isNull, inArray } from 'drizzle-orm';
// 複数条件の AND
const activeAdults = await db
.select()
.from(users)
.where(
and(
gte(users.age, 18),
eq(users.isActive, true)
)
);
// OR 条件
const result = await db
.select()
.from(users)
.where(
or(
like(users.name, '田中%'),
ilike(users.email, '%example.com')
)
);
// IN 句
const specificUsers = await db
.select()
.from(users)
.where(inArray(users.id, [1, 2, 3]));
// NULL チェック
const noAge = await db
.select()
.from(users)
.where(isNull(users.age));
2. JOIN(結合クエリ)
// INNER JOIN
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
// LEFT JOIN
const usersWithPosts = await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.authorId));
3. Relational Queries(リレーショナルクエリ API)
スキーマにリレーションを定義すると、Prisma ライクなネスト取得が可能になります。
import { relations } from 'drizzle-orm';
// schema.ts にリレーション定義を追加
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// リレーショナルクエリで取得
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
where: eq(users.isActive, true),
limit: 10,
});
// 特定の1件を取得
const singleUser = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
limit: 5,
},
},
});
4. トランザクション
const result = await db.transaction(async (tx) => {
const [newUser] = await tx.insert(users).values({
name: '佐藤花子',
email: 'sato@example.com',
age: 25,
}).returning();
await tx.insert(posts).values({
title: '初めての投稿',
content: 'こんにちは!',
authorId: newUser.id,
});
return newUser;
});
// ネストされたトランザクション(セーブポイント)
await db.transaction(async (tx) => {
await tx.insert(users).values({ name: 'A', email: 'a@example.com' });
try {
await tx.transaction(async (nestedTx) => {
await nestedTx.insert(users).values({ name: 'B', email: 'b@example.com' });
throw new Error('ロールバックテスト');
});
} catch {
// ネストされたトランザクションのみロールバック。ユーザー A は残る
}
});
5. 集計・ソート・ページネーション
import { count, avg, sum, sql } from 'drizzle-orm';
// 集計
const stats = await db
.select({
totalUsers: count(),
averageAge: avg(users.age),
})
.from(users);
// GROUP BY
const postCountByAuthor = await db
.select({
authorId: posts.authorId,
postCount: count(posts.id),
})
.from(posts)
.groupBy(posts.authorId);
// ORDER BY + LIMIT + OFFSET(ページネーション)
const page = 1;
const pageSize = 20;
const paginatedUsers = await db
.select()
.from(users)
.orderBy(users.createdAt)
.limit(pageSize)
.offset((page - 1) * pageSize);
// 生 SQL を混ぜることも可能
const customQuery = await db
.select({
id: users.id,
nameUpper: sql<string>`UPPER(${users.name})`,
})
.from(users);
類似パッケージとの比較
| 特徴 | Drizzle ORM | Prisma | Kysely | TypeORM |
|---|---|---|---|---|
| クエリスタイル | SQL ライク | 独自 DSL | SQL ライク | デコレータ / QueryBuilder |
| 型安全性 | ◎(推論ベース) | ◎(コード生成) | ◎(推論ベース) | △ |
| バンドルサイズ | 非常に小さい | 大きい(Engine 含む) | 小さい | 大きい |
| エッジランタイム対応 | ◎ | △(一部対応) | ○ | × |
| マイグレーション | drizzle-kit | prisma migrate | 別途必要 | 内蔵 |
| リレーショナルクエリ | ◎(with 構文) | ◎(include) | △(手動 JOIN) | ○ |
| 学習コスト | 低(SQL 知識が活きる) | 中 | 低 | 高 |
| GUI ツール | Drizzle Studio | Prisma Studio | なし | なし |
| 生 SQL サポート | ◎(sql テンプレート) | ○($queryRaw) | ◎ | ○ |
選定の目安:
- SQL に慣れている+軽量さ重視 → Drizzle ORM
- スキーマファーストで素早く開発したい → Prisma
- クエリビルダーだけ欲しい(ORM 不要) → Kysely
- Java/C# 出身でデコレータに慣れている → TypeORM
注意点・Tips
1. スキーマ定義はデータベース方言ごとに異なる
// PostgreSQL
import { pgTable, serial, text } from 'drizzle-orm/pg-core';
// MySQL
import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core';
// SQLite
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
DB を切り替える場合、スキーマ定義の書き直しが必要です。抽象化レイヤーは提供されていません。
2. returning() は PostgreSQL と SQLite のみ
MySQL は INSERT ... RETURNING をサポートしていないため、returning() は使えません。MySQL では $returningId() を使用します。
// MySQL の場合
const result = await db.insert(users).values({ name: '田中' }).$returningId();
3. drizzle-kit push と drizzle-kit migrate の使い分け
push— マイグレーションファイルを生成せず、スキーマを直接 DB に反映。プロトタイピングやローカル開発向け。generate→migrate— マイグレーション SQL を生成してからバージョン管理。本番運用向け。
4. 型の取得(InferSelectModel / InferInsertModel)
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
// SELECT 結果の型
type User = InferSelectModel<typeof users>;
// INSERT 時の型(default 付きカラムは optional になる)
type NewUser = InferInsertModel<typeof users>;
5. $with によるCTE(共通テーブル式)
const activeUsers = db.$with('active_users').as(
db.select().from(users).where(eq(users.isActive, true))
);
const result = await db
.with(activeUsers)
.select()
.from(activeUsers);
6. パフォーマンス Tips
- Prepared Statements を活用すると、同じクエリの繰り返し実行が高速化されます。
const prepared = db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare('get_user_by_id');
const user = await prepared.execute({ id: 1 });
- **
selectで