Claude CodeでDrizzle ORMを使う — スキーマ定義とマイグレーションの実装フロー
Claude CodeでDrizzle ORMのスキーマ定義からdrizzle-kitでのマイグレーション生成、CLAUDE.mdでの規約固定までをまとめます。
Claude CodeでDrizzle ORMを使うとは
Drizzle ORMは、データベースのスキーマをTypeScriptのコードとして定義するORMです。SQLに近い書き方を保ったまま型を効かせるのが特徴で、スキーマの定義そのものがクエリとマイグレーションの両方の元になります。
Claude Codeと組み合わせると、スキーマファイルの追記やdrizzle-kitコマンドの実行、生成されたSQLの読み解きまでを一括で進められます。ただしデータベースへの適用まで任せきりにはできません。マイグレーションはデータを壊しうる操作なので、Claude Codeに書かせたコードと生成されたSQLを毎回人間が確認する運用が前提になります。
環境を整える — ドライバの選択とパッケージ導入
Drizzle ORMはPostgreSQL・MySQL・SQLiteなど複数のダイアレクトに対応しますが、接続方式によって導入するパッケージが変わります。PostgreSQLの場合、対応するドライバはnode-postgres(pg)とpostgres.jsの2つです。
npm i drizzle-orm pg
npm i -D drizzle-kit @types/pg接続コードは1行で済みます。既存のプールを渡すこともでき、環境変数から接続文字列だけ渡す最小構成と、Poolインスタンスを自分で管理する構成の両方がサンプルとして示されています。
import { drizzle } from "drizzle-orm/node-postgres";
const db = drizzle(process.env.DATABASE_URL);
const result = await db.execute("select 1");postgres.jsを選ぶ場合はパッケージ名がpostgresに変わるだけで、以降のdrizzle-kitの使い方は同一です。どちらのドライバでも、Claude Codeに接続コードを書かせるときは環境変数名(DATABASE_URLなど)をCLAUDE.mdか指示文で明示しておくと、存在しない変数名を書いてしまう事故を防げます。
2つのドライバには細かな違いもあります。node-postgresはpg-nativeを追加インストールすると速度が約10%向上し、クエリ単位で型パーサーを指定できます。一方postgres.jsは既定でプリペアドステートメントを使うため、AWS環境などでは明示的にオプトアウトが必要になる場合があります。どちらを選ぶかは、パフォーマンス要件と実行環境の制約次第です。
スキーマをTypeScriptで定義する
テーブルはpgTable(MySQLはmysqlTable、SQLiteはsqliteTable)で定義します。カラムの型・制約はメソッドチェーンで書き、この定義がクエリの型とマイグレーションSQLの両方に反映されます。
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar().notNull(),
age: integer().notNull(),
email: varchar().notNull().unique(),
});個別importとコールバック引数((t) => ({...}))を使う書き方の2つが、スキーマ定義のサンプルとして示されています。挙動は同じなので、既存プロジェクトに合わせて片方に統一するのが実務上のポイントです。Claude Codeにテーブルを追記させるときは、既存のschema.tsファイル自体をコンテキストに含めて「このファイルと同じ書き方で追記して」と指示すると、import形式とコールバック形式が1ファイル内で混在せずに済みます。
drizzle-kitでマイグレーションを生成する場合は、モデルをすべてexportしておく必要があります。exportし忘れたテーブルは、次で説明するgenerateコマンドの差分検出の対象から漏れます。
drizzle-kitでマイグレーションを管理する
drizzle-kitはDrizzleのマイグレーションを扱うCLIです。安定版のインストールコマンドに@rcは付きません。
npm i -D drizzle-kit設定はdrizzle.config.tsに書きます。必須なのは接続先のSQLダイアレクトとスキーマファイルのパスの2つです。
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./src/schema.ts",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});generateはスキーマの差分だけを見るのでdialectとschemaがあれば動きますが、migrate・push・pullは実際のデータベースに接続するためdbCredentials(接続先のURL)も必須です。
代表的なコマンドは次の6つで、それぞれ役割が分かれています。ほかに、既存マイグレーションのスナップショットを上げるupと、スキーマをSQLのDDLとして出力するexportもあります。
npx drizzle-kit generate # スキーマの差分からSQLマイグレーションファイルを作る
npx drizzle-kit migrate # 生成済みのSQLファイルをDBへ適用する
npx drizzle-kit push # SQLファイルを作らず、スキーマを直接DBへ反映する
npx drizzle-kit pull # DBの現在のスキーマをTypeScriptへ書き出す
npx drizzle-kit check # 生成済みマイグレーション同士の衝突を確認する
npx drizzle-kit studio # DBブラウジング用のプロキシサーバーを立てるClaude Codeに任せる範囲は、schema.tsの編集とgenerateの実行までに絞るのが安全です。migrateやpushは実際のデータベースに変更を加えるコマンドなので、生成されたSQLファイルの内容を人間が読んだうえで実行する運用にします。
Push・Generate+Migrate・Pullの使い分け
3つのコマンド系列は、スキーマとデータベースのどちらを「正」にするかで選び方が変わります。データベース側のスキーマを正として管理する「データベースファースト」ならpull、コードベース側のTypeScriptスキーマを正とする「コードベースファースト」ならgenerateまたはpushが起点になります。
| 用途 | おすすめ度 | 理由 |
|---|---|---|
| 個人検証・プロトタイプの高速な反復 | おすすめ度◎ | 理由pushはSQLファイルを作らず即時反映できる |
| チーム開発・本番環境への適用 | おすすめ度◎(generate+migrate) | 理由生成されたSQLをレビューしてから適用できる |
| 既存DBをコードベースに取り込む | おすすめ度◎(pull) | 理由DBのスキーマをpullでTypeScriptへ変換できる |
本番DBにpushを直接使う | おすすめ度△ | 理由差分がレビューされずそのまま反映されるためトラブル発生時に原因を追いにくい |
複数人が同時にブランチでスキーマを変更するチームでは、checkコマンドも併用します。生成済みのマイグレーション同士が競合(同じ変更を別々のファイルに書いてしまうケース)していないかを検査でき、generateだけでは気づけない衝突をmigrateの前に拾えます。
CLAUDE.mdにスキーマの規約を書く
Claude CodeのCLAUDE.mdは、「Claudeが同じ間違いを2回目に繰り返したとき」に書き足す場所として位置づけられています。Drizzleのスキーマ運用でも、この基準がそのまま当てはまります。
例えば以下のような内容をCLAUDE.mdに書いておくと、Claude Codeが毎回同じ質問をしたり、命名規則を無視したcamelCaseのカラムを紛れ込ませたりせずに済みます。
## データベーススキーマ
- スキーマ定義は `src/schema.ts` に集約する(ファイルを分割しない)
- カラム名はスネークケース、TypeScript側のプロパティ名はキャメルケース
- スキーマを変更したら `npx drizzle-kit generate` を実行し、
生成された `drizzle/` 配下のSQLを確認してから報告する
- `drizzle-kit push` と `drizzle-kit migrate` は本番DBに向けて実行しないCLAUDE.mdはコンテキストとして読み込まれるだけで、強制力を持つ設定ではありません。「実行しない」と書いても、Claude Codeが誤ってpushやmigrateを実行する可能性は残ります。確実に止めたい操作は、次のHooksで機械的にブロックします。
Hooksで本番マイグレーションの暴走を防ぐ
CLAUDE.mdの指示はコンテキストであり設定ではないため、破壊的な操作を確実に止めたいならPreToolUseフックで実行前にブロックします。rm -rfを止める例と同じ仕組みで、drizzle-kit pushやmigrateも対象にできます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(*drizzle-kit push*)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-prod-push.sh"
},
{
"type": "command",
"if": "Bash(*drizzle-kit migrate*)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-prod-push.sh"
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/block-prod-push.sh
if [ "$NODE_ENV" = "production" ]; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"本番環境へのdrizzle-kit push/migrateはブロックされています"}}'
exit 0
fimatcherでBashツールの呼び出しに絞り、if条件でdrizzle-kit pushまたはdrizzle-kit migrateを含むコマンドだけをスクリプトに渡す形です。実際に実行されるのはnpx drizzle-kit pushのように先頭にnpxが付くコマンドなので、ifのパターンは前後にワイルドカード(*)を置いて部分一致にする必要があります。先頭一致(Bash(drizzle-kit push*))のままだとnpx付きのコマンドに一致せず、フックが発火しません。
このスクリプトは標準入力を読まず、Claude Codeプロセス自体が持つ環境変数$NODE_ENVだけを判定基準にしています。コマンド側にだけNODE_ENV=production npx drizzle-kit pushのように付けた値はこのスクリプトに届かないので、本番相当の環境で確実に止めたいなら、Claude Codeを起動するプロセスの環境変数としてNODE_ENV=productionを事前にエクスポートしておく必要があります。接続先のデータベースURLで判定したい場合は、標準入力からJSONを受け取りtool_input.commandをjqで読む実装に変える必要があります。
リレーションと型安全なクエリ
ネストした関連データを取得するAPIとして、Drizzleはdb.query.テーブル名.findMany()という形のRelational Queriesを提供しています。
const result = await db.query.posts.findMany({
with: {
author: true,
},
});このクエリの前提になるリレーション定義は、バージョンによって書き方が違います。npmのlatestが指す安定版(0.45.3)では、relations関数でテーブルごとにリレーションを宣言し、drizzleに渡す設定のschemaへテーブルとまとめて渡します。
import { drizzle } from "drizzle-orm/node-postgres";
import { relations } from "drizzle-orm";
import { usersTable, postsTable } from "./schema";
export const postsRelations = relations(postsTable, ({ one }) => ({
author: one(usersTable, {
fields: [postsTable.ownerId],
references: [usersTable.id],
}),
}));
const schema = { usersTable, postsTable, postsRelations };
const db = drizzle(process.env.DATABASE_URL, { schema });@rc(1.0.0-rc.4)では書き方が変わります。Relationsページの例はdefineRelations関数を使う書き方に統一されており、テーブル定義とリレーション定義を1つの関数呼び出しにまとめます。
import { defineRelations } from "drizzle-orm";
const relations = defineRelations({ users, posts }, (r) => ({
posts: {
author: r.one.users({
from: r.posts.ownerId,
to: r.users.id,
}),
},
}));
const db = drizzle({ client, relations });既存プロジェクトのリレーション定義が安定版の書き方になっている場合、RelationsページのdefineRelationsサンプルをそのまま貼り付けると動きません。Claude Codeにリレーションを書かせる前に、package.jsonに入っているバージョンを確認させ、対応するAPIに合わせて書かせるのが安全です。
Relational Queriesには、コールバック内の参照方法について「Important」と明記された制約が1つあります。orderByやwhereのコールバック内では、importしたテーブルオブジェクトを直接参照せず、コールバックが渡す引数越しに参照する必要があります。
よくあるつまずき
@rcをそのまま本番に入れてしまう。Get Startedページのコマンドを疑わずコピーすると、意図せずリリース候補版がインストールされます。package.jsonのバージョン表記を都度確認する習慣が要ります。
pushをチーム共有DBに向けてしまう。ローカル検証用のつもりで実行したコマンドが、drizzle.config.tsの接続先が共有ステージング環境を向いたままだと、他の開発者のテストデータをまとめて書き換えます。接続先のURLは実行前に必ず確認します。
モデルのexport漏れ。drizzle-kitはexportされたモデルしか差分検出の対象にしません。新しいテーブルを追加してexportを忘れると、generateを実行しても何も生成されません。
MCP経由のDB接続と混同する。Claude CodeにMCPサーバー経由でデータベースへ直接接続させる構成は、本記事のスキーマ管理とは別の権限設計が必要です。読み取り専用の担保方法は製品ごとに違うので、データベースMCPサーバーの読み取り専用設定を5製品で比較するで扱っている前提を先に確認しておくと、スキーマ管理とMCP接続の権限を取り違えません。
まとめ
Drizzle ORMのスキーマ定義とdrizzle-kitのCLIコマンドは、安定版でも@rcでも共通です。違うのはリレーション定義のAPIだけなので、プロジェクトのバージョンを確認してから公式サンプルをコピーすれば、Claude Codeに書かせるコードと実際のAPIのずれを防げます。CLAUDE.mdでの規約共有とHooksでの機械的なブロックを両方使うと、コンテキストとしての指示と、実行前に必ず効く制御を分けて運用できます。
CLAUDE.mdへの規約の書き方は、Next.js SaaS MVPの構築ガイドでも同じ考え方を扱っています。AGENTS.mdとCLAUDE.mdを両方使うプロジェクトなら、AGENTS.mdとCLAUDE.mdの設定統合パターンも参考になります。バックエンド側で型を軸にした実装フローを組む発想は、Claude CodeでFastAPIバックエンドを構築するとも共通しています。