drizzle-orm について
Drizzle ORMは、データベースを使うアプリケーション向けのTypeScriptファーストなSQL ORMです。エッジやサーバーレス環境も想定し、型推論とSQL風クエリに加えて、スキーマ定義、トランザクション、マイグレーションツールを備えています。
主な機能
- 01TypeScriptの型推論
- 02SQL風クエリAPI
- 03スキーマとリレーションの定義
- 04トランザクション
- 05drizzle-kitによるマイグレーション
活用シーン
- 01型付きデータベーススキーマの定義
- 02リレーショナルデータの検索と更新
- 03PostgreSQLスキーマのマイグレーション管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thatjuan/ agent-skills --skill drizzle-ormインストール後、Claude Code で/drizzle-ormを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreferences/advanced-schemas.mdperformance.mdpg-migrations.mdquery-patterns.mdvs-prisma.md詳細
Drizzle ORM
TypeScript を第一に考えた SQL ORM です。依存関係がなく、コンパイル時の型安全性と SQL に似た構文を備えています。エッジランタイム、サーバーレス環境、PostgreSQL 向けに最適化されています。
インストール
# Core ORM + PostgreSQL driver
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
# Alternative drivers
npm install postgres # postgres.js
npm install @neondatabase/serverless # Neon serverless
npm install @electric-sql/pglite # PGlite embedded
データベース接続
// Simple connection string
import { drizzle } from 'drizzle-orm/node-postgres';
const db = drizzle(process.env.DATABASE_URL!);
// With connection pool
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
const db = drizzle({ client: pool });
// With schema for relational queries
import * as schema from './schema';
const db = drizzle({ client: pool, schema });
// With SSL
const db = drizzle({
connection: { connectionString: process.env.DATABASE_URL!, ssl: true }
});
スキーマ定義(PostgreSQL)
テーブル宣言
import { pgTable, serial, text, varchar, integer, boolean,
timestamp, json, jsonb, uuid } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
email: varchar({ length: 255 }).notNull().unique(),
name: text().notNull(),
role: text({ enum: ['admin', 'user', 'guest'] }).default('user'),
metadata: jsonb().$type<{ theme: string; locale: string }>(),
isActive: boolean().default(true),
createdAt: timestamp().defaultNow().notNull(),
updatedAt: timestamp().defaultNow().notNull(),
});
型推論
type User = typeof users.$inferSelect; // SELECT result type
type NewUser = typeof users.$inferInsert; // INSERT input type
PostgreSQL のカラム型
| 型 | インポート | TypeScript | 備考 |
|---|---|---|---|
integer() | pg-core | number | 4 バイト符号付き |
smallint() | pg-core | number | 2 バイト符号付き |
bigint({ mode: 'number' }) | pg-core | number | 8 バイト。mode で JS の型を選択 |
serial() | pg-core | number | 自動インクリメント、4 バイト |
bigserial({ mode: 'number' }) | pg-core | number | 自動インクリメント、8 バイト |
text() | pg-core | string | 長さの制限なし |
varchar({ length: 255 }) | pg-core | string | 最大長を指定する可変長 |
char({ length: 10 }) | pg-core | string | 固定長 |
boolean() | pg-core | boolean | |
timestamp() | pg-core | Date | { withTimezone: true }、{ mode: 'string' } |
date() | pg-core | Date | 生の値には { mode: 'string' } |
time() | pg-core | string | { withTimezone: true } |
interval() | pg-core | string | { fields: 'day' } |
json() | pg-core | unknown | .$type<T>() を使用 |
jsonb() | pg-core | unknown | バイナリ JSON。.$type<T>() を使用 |
uuid() | pg-core | string | 自動生成には .defaultRandom() |
numeric({ precision, scale }) | pg-core | string | JS の number 型には { mode: 'number' } |
real() | pg-core | number | 4 バイト浮動小数点数 |
doublePrecision() | pg-core | number | 8 バイト浮動小数点数 |
bytea() | pg-core | Buffer | バイナリデータ |
point() | pg-core | [number, number] | オブジェクトには { mode: 'xy' } |
列挙型
import { pgEnum } from 'drizzle-orm/pg-core';
export const roleEnum = pgEnum('role', ['admin', 'user', 'guest']);
export const users = pgTable('users', {
id: serial().primaryKey(),
role: roleEnum().default('user'),
});
IDENTITY カラム(PostgreSQL 10 以降)
// Preferred over serial() for new schemas
id: integer().primaryKey().generatedAlwaysAsIdentity({ startWith: 1000 }),
id: integer().primaryKey().generatedByDefaultAsIdentity(),
インデックスと制約
import { pgTable, serial, text, integer, index, uniqueIndex,
primaryKey, foreignKey, unique, check } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
export const posts = pgTable('posts', {
id: serial().primaryKey(),
title: text().notNull(),
authorId: integer().notNull().references(() => users.id, { onDelete: 'cascade' }),
slug: text().notNull(),
status: text({ enum: ['draft', 'published'] }),
}, (table) => [
index('title_idx').on(table.title),
uniqueIndex('slug_idx').on(table.slug),
check('status_check', sql`${table.status} in ('draft', 'published')`),
]);
// Composite primary key (junction tables)
export const usersToGroups = pgTable('users_to_groups', {
userId: integer().notNull().references(() => users.id),
groupId: integer().notNull().references(() => groups.id),
}, (table) => [
primaryKey({ columns: [table.userId, table.groupId] }),
]);
// Composite unique constraint
export const example = pgTable('example', {
a: integer(),
b: text(),
}, (table) => [
unique('ab_unique').on(table.a, table.b),
unique('nulls_example').on(table.a).nullsNotDistinct(), // PG 15+
]);
リレーション
リレーションはデータベースの外部キーとは別の、アプリケーションレベルの抽象化です。リレーショナルクエリー API(db.query)を利用できます。
import { relations } from 'drizzle-orm';
// One-to-many
export const authorsRelations = relations(authors, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(authors, {
fields: [posts.authorId],
references: [authors.id],
}),
}));
// Many-to-many (through junction table)
export const usersRelations = relations(users, ({ many }) => ({
usersToGroups: many(usersToGroups),
}));
export const usersToGroupsRelations = relations(usersToGroups, ({ one }) => ({
user: one(users, { fields: [usersToGroups.userId], references: [users.id] }),
group: one(groups, { fields: [usersToGroups.groupId], references: [groups.id] }),
}));
// Disambiguating multiple relations to same table
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId], references: [users.id],
relationName: 'author',
}),
reviewer: one(users, {
fields: [posts.reviewerId], references: [users.id],
relationName: 'reviewer',
}),
}));
クエリー
SELECT
import { eq, ne, gt, gte, lt, lte, like, ilike, inArray,
isNull, isNotNull, and, or, between, sql, desc, asc, count } from 'drizzle-orm';
// Basic
const allUsers = await db.select().from(users);
// Partial select
const names = await db.select({ id: users.id, name: users.name }).from(users);
// Filtering
await db.select().from(users).where(eq(users.email, '[email protected]'));
await db.select().from(users).where(
and(eq(users.role, 'admin'), gt(users.createdAt, new Date('2024-01-01')))
);
await db.select().from(users).where(inArray(users.id, [1, 2, 3]));
// Sorting & pagination
await db.select().from(users).orderBy(desc(users.createdAt)).limit(10).offset(20);
// Distinct / Distinct On (PostgreSQL)
await db.selectDistinct().from(users);
await db.selectDistinctOn([users.id]).from(users).orderBy(users.id);
INSERT
// Single row with returning
const [newUser] = await db.insert(users)
.values({ email: '[email protected]', name: 'John' })
.returning();
// Multiple rows
await db.insert(users).values([
{ email: '[email protected]', name: 'A' },
{ email: '[email protected]', name: 'B' },
]);
// Upsert
await db.insert(users)
.values({ id: 1, email: '[email protected]', name: 'Dan' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'Dan Updated' },
});
await db.insert(users)
.values({ id: 1, email: '[email protected]', name: 'Dan' })
.onConflictDoNothing({ target: users.email });
// Insert from select
await db.insert(employees).select(
db.select({ name: users.name }).from(users).where(eq(users.role, 'employee'))
);
UPDATE
await db.update(users).set({ name: 'Jane' }).where(eq(users.id, 1));
await db.update(users).set({ updatedAt: sql`NOW()` }).where(eq(users.id, 1));
// With returning
const updated = await db.update(users)
.set({ name: 'Jane' })
.where(eq(users.id, 1))
.returning();
// UPDATE...FROM
await db.update(users)
.set({ cityId: cities.id })
.from(cities)
.where(and(eq(cities.name, 'Seattle'), eq(users.name, 'John')));
DELETE
await db.delete(users).where(eq(users.id, 1));
const deleted = await db.delete(users).where(eq(users.id, 1)).returning();
JOIN
// Inner join
const result = await db.select({ user: users, post: posts })
.from(users)
.innerJoin(posts, eq(users.id, posts.authorId));
// Left join (pet fields become nullable)
await db.select().from(users).leftJoin(posts, eq(users.id, posts.authorId));
// Aggregation with join
await db.select({
authorName: authors.name,
postCount: count(posts.id),
}).from(authors)
.leftJoin(posts, eq(authors.id, posts.authorId))
.groupBy(authors.id);
// Self-join with alias
import { alias } from 'drizzle-orm/pg-core';
const parent = alias(users, 'parent');
await db.select().from(users).leftJoin(parent, eq(parent.id, users.parentId));
リレーショナルクエリー
// Requires schema passed to drizzle() constructor
const usersWithPosts = await db.query.users.findMany({
with: { posts: { with: { comments: true } } },
columns: { id: true, name: true },
where: (users, { eq }) => eq(users.role, 'admin'),
orderBy: (users, { desc }) => [desc(users.createdAt)],
limit: 10,
});
const user = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, 1),
with: { posts: true },
});
トランザクション
// Auto-rollback on error
await db.transaction(async (tx) => {
await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100` })
.where(eq(accounts.id, 1));
await tx.update(accounts).set({ balance: sql`${accounts.balance} + 100` })
.where(eq(accounts.id, 2));
});
// Conditional rollback
await db.transaction(async (tx) => {
const [account] = await tx.select().from(accounts).where(eq(accounts.id, 1));
if (account.balance < 100) {
tx.rollback();
return;
}
await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100` })
.where(eq(accounts.id, 1));
});
// Nested transactions (savepoints)
await db.transaction(async (tx) => {
await tx.insert(users).values({ name: 'John', email: '[email protected]' });
await tx.transaction(async (tx2) => {
await tx2.insert(posts).values({ title: 'Post', authorId: 1 });
});
});
// PostgreSQL isolation levels
await db.transaction(async (tx) => { /* ... */ }, {
isolationLevel: 'serializable',
accessMode: 'read write',
deferrable: true,
});
drizzle-kit によるマイグレーション
設定
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './drizzle',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
主なコマンド
# Generate SQL migration from schema changes
npx drizzle-kit generate
# Apply migrations to database
npx drizzle-kit migrate
# Push schema directly (no migration files — for rapid prototyping)
npx drizzle-kit push
# Pull existing database schema into TypeScript
npx drizzle-kit pull
# Generate custom empty migration
npx drizzle-kit generate --custom --name=seed-users
# Verify migration consistency
npx drizzle-kit check
# Launch visual database browser
npx drizzle-kit studio
プログラムからのマイグレーション
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
const db = drizzle(process.env.DATABASE_URL!);
await migrate(db, { migrationsFolder: './drizzle' });
マイグレーション戦略、チームのワークフロー、本番環境でのパターンについて詳しくは、PostgreSQL マイグレーション を参照してください。
PostgreSQL スキーマ
import { pgSchema, serial, text } from 'drizzle-orm/pg-core';
export const tenantSchema = pgSchema('tenant_1');
export const tenantUsers = tenantSchema.table('users', {
id: serial().primaryKey(),
name: text().notNull(),
});
// Generates: CREATE SCHEMA "tenant_1"; CREATE TABLE "tenant_1"."users" (...)
ビュー
import { pgView, pgMaterializedView } from 'drizzle-orm/pg-core';
export const activeUsers = pgView('active_users').as((qb) =>
qb.select().from(users).where(eq(users.isActive, true))
);
export const userStats = pgMaterializedView('user_stats').as((qb) =>
qb.select({
userId: users.id,
postCount: count(posts.id),
}).from(users).leftJoin(posts, eq(users.id, posts.authorId)).groupBy(users.id)
);
// Refresh at runtime
await db.refreshMaterializedView(userStats);
await db.refreshMaterializedView(userStats).concurrently();
共通テーブル式(CTE)
const sq = db.$with('active').as(
db.select().from(users).where(eq(users.isActive, true))
);
const result = await db.with(sq).select().from(sq);
// CTE with INSERT
const inserted = db.$with('new_user').as(
db.insert(users).values({ name: 'John', email: '[email protected]' }).returning()
);
await db.with(inserted).select().from(inserted);
注意すべき点
| 問題 | リスク |
|---|---|
.$type<T>() を指定せずに JSON カラムで any/unknown を使用する | 型安全性が失われます |
sql テンプレートの代わりに文字列を直接連結する | SQL インジェクション |
db.transaction() の外で複数ステップの変更を行う | 部分的な書き込み |
大きなテーブルで .limit() なしに select() を使用する | メモリ枯渇 |
| 外部キーカラムにインデックスがない | JOIN が遅くなります |
| 頻繁に絞り込みに使うカラムにインデックスがない | テーブル全体のスキャン |
generatedAlwaysAsIdentity() の代わりに serial() を使用する | serial は PG 10 以降では旧式です |
参考資料
-
PostgreSQL のマイグレーション — drizzle-kit の設定、マイグレーション戦略、チームのワークフロー、本番環境でのパターン、カスタムマイグレーション、プログラムによるマイグレーション API。データベーススキーマの変更を管理する際に読み込んでください。
-
高度なスキーマ — カスタム型、ブランド型、複合キー、配列、複数スキーマ、ビュー、マテリアライズドビュー、pgEnum のパターン。複雑なデータベーススキーマを設計する際に読み込んでください。
-
クエリのパターン — サブクエリ、CTE、生 SQL、プリペアドステートメント、動的クエリ、バッチ処理、集計、ウィンドウ関数。クエリを最適化する場合や、複雑なフィルタリングを扱う際に読み込んでください。
-
パフォーマンス — コネクションプーリング、プリペアドステートメント、N+1 の防止、クエリの最適化、エッジランタイムとの統合、サーバーレスのパターン。データベースのパフォーマンスを拡張または最適化する際に読み込んでください。
-
Drizzle と Prisma の比較 — 機能比較、マイグレーションガイド、アーキテクチャの違い、Drizzle を選ぶべき状況。ORM を評価する場合や、Prisma から移行する際に読み込んでください。