Claude Media
Claude CodeでDrizzle ORMを使う — スキーマ定義とマイグレーションの実装フロー

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-kit 0.31.11とdrizzle-orm 0.45.3(どちらもnpmのlatest)、PostgreSQL向けの設定のものです。任せる範囲と人間が見る箇所の分け方は、次の4段階が基本になります。

手順

Claude Codeに任せる範囲

  1. 1

    schema.tsを編集させる

    テーブルとカラムの追加は、既存の書き方に合わせて追記させます。

  2. 2

    generateを実行させる

    drizzle-kit generateはスキーマの差分からSQLファイルを作るだけで、DBには触りません。

  3. 3

    生成されたSQLを人間が読む

    DROP COLUMNが混ざっていないかを、適用の前にここで見ます。

  4. 4

    migrateは人間が実行する

    DBを書き換えるのはここだけにして、接続先のURLも実行前に見ます。

環境を整える — ドライバの選択とパッケージ導入

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に、import先がdrizzle-orm/postgres-jsに変わりますが、以降のdrizzle-kitの使い方は同一です。node-postgresではpg-nativeを入れると、node-postgresとDrizzleの両方が約10%速くなるとPostgreSQL向けのGet Startedに書かれています。postgres.jsは既定でプリペアドステートメントを使うため、AWS環境では無効化が必要になる場合がある、とも同じページにあります。どちらのドライバでも、Claude Codeに接続コードを書かせるときは環境変数名(DATABASE_URLなど)をCLAUDE.mdか指示文で明示しておくと、存在しない変数名を書いてしまう事故を防げます。

スキーマを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コマンドの差分検出の対象から漏れます。

usersのほかにexportなしのhiddenテーブルを同じファイルに置いてgenerateを実行すると、出力は次のとおりでした。hiddenは数えられず、SQLファイルにも現れません。

npx drizzle-kit generate
1 tables
users 4 columns 0 indexes 0 fks
 
[✓] Your SQL migration file ➜ drizzle/0000_groovy_black_cat.sql 🚀

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)も必須です。

DATABASE_URLを未設定のままmigrateを実行すると、DBに触れる前に次のエラーで止まります。

Error  Please provide required params for Postgres driver:
    [x] url: undefined

drizzle-kit --helpの一覧は、generate・migrate・introspect・push・studio・up・check・drop・exportの9つです。pullはintrospectの別名で、drizzle-kit pull --helpの先頭にもAliases: introspect, pullと出ます。代表的なコマンドの役割は次のとおりです。ほかに、スナップショットを上げる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ブラウジング用のプロキシサーバーを立てる

migrateとpushは、どちらも実際のデータベースを書き換えます。pushはSQLファイルを残さないので、人間が事前に読める確認材料そのものがありません。

Push・Generate+Migrate・Pullの使い分け

3つのコマンド系列は、スキーマとデータベースのどちらを「正」にするかで選び方が変わります。データベース側のスキーマを正として管理する「データベースファースト」ならpull、コードベース側のTypeScriptスキーマを正とする「コードベースファースト」ならgenerateまたはpushが起点になります。

くらべる

generate+migrateとpushの違い

SQLを残してレビューしたいとき

generate + migrate

SQLファイルがリポジトリに残るので、適用前にレビューでき、履歴も追えます。

素早く反映したいとき

push

SQLファイルを作らず、差分をそのままDBへ反映します。migrationsのページはpushを素早いプロトタイピングに最適な方法とし、本番アプリケーションで主要なマイグレーション手段として使うチームや個人開発者の例もあると書いています。この記事の運用では、SQLを人間が読んでから適用するため、generate+migrateを軸にします。

既存のDBをコードベースに取り込むときはpullでDBのスキーマをTypeScriptへ書き出します。

複数人が同時にブランチでスキーマを変更するチームでは、checkコマンドも併用します。生成済みのマイグレーション同士が競合(別ブランチで生成されたマイグレーションが衝突するケース)していないかを検査でき、generateだけでは気づけない衝突をmigrateの前に拾えます。

生成されたSQLのどこを読むか

人間が読むべきSQLの実例を、usersテーブルの変更2回分で示します。1回目はnicknameカラムの追加です。

ALTER TABLE "users" ADD COLUMN "nickname" varchar;

2回目は、スキーマからageを消してgenerateを実行した結果です。確認を求める表示は出ず、DROP COLUMNのSQLファイルがそのまま作られました。

ALTER TABLE "users" DROP COLUMN "age";

データが入ったテーブルでこのファイルをmigrateすると、ageの値は戻りません。Claude Codeにgenerateまで任せる場合も、drizzle/に増えたファイルのDROPとALTER ... TYPEは人間が目で拾います。

もう1つ、追加と削除を1回にまとめた変更には注意が要ります。ageを消してhandleを足したスキーマでgenerateを実行したところ、drizzle-kitは「カラムの追加か、改名か」を端末上で尋ねようとしました。標準入力がつながっていない環境では、次のエラーで止まります。

Error: Interactive prompts require a TTY terminal (process.stdin.isTTY or process.stdout.isTTY is false).
This can happen when running in CI, piped input, or non-interactive shells.

カラムの追加と削除は別々の変更に分けてgenerateし、1回ごとにSQLを読む流れにすると、この問い合わせを避けられます。

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はコンテキストとして読み込まれるだけなので、「実行しない」と書いてもpushやmigrateが走る余地は残ります。確実に止めたい操作は、次のHooksで機械的にブロックします。

Hooksで本番マイグレーションの暴走を防ぐ

破壊的な操作を確実に止めたいなら、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
fi

matcherでBashツールの呼び出しに絞り、if条件でdrizzle-kit pushまたはdrizzle-kit migrateを含むコマンドだけをスクリプトに渡す形です。実際に実行されるのはnpx drizzle-kit pushのように先頭にnpxが付くコマンドなので、ifのパターンは前後にワイルドカード(*)を置いて部分一致にする必要があります。先頭一致(Bash(drizzle-kit push*))のままだとnpx付きのコマンドに一致せず、フックが発火しません。

ifの照合では、コマンド先頭のVAR=value形式の代入は取り除かれ、&&でつないだコマンドも1つずつ判定されます。NODE_ENV=production npx drizzle-kit pushやnpm test && npx drizzle-kit pushも、上のパターンに一致します。

このスクリプトは標準入力を読まず、Claude Codeプロセス自体が持つ環境変数$NODE_ENVだけを判定基準にしています。コマンド側にだけNODE_ENV=production npx drizzle-kit pushのように付けた値はこのスクリプトに届かないので、本番相当の環境で確実に止めたいなら、Claude Codeを起動するプロセスの環境変数としてNODE_ENV=productionを事前にエクスポートしておく必要があります。接続先のデータベースURLで判定したい場合は、標準入力からJSONを受け取りtool_input.commandをjqで読む実装に変える必要があります。

ifによる絞り込みは、公式ドキュメントでも「ベストエフォート」と書かれています。実行コマンドの中身を判定できない場合は、パターンにかかわらずフックが走ります。絞り込みの精度に左右されず確実に止めたいコマンドは、settings.jsonのpermissions.denyに書く方法があります。環境変数のように条件を見て止めるかどうかを決めたいときは、フックの出番です。

{
  "permissions": {
    "deny": ["Bash(npx drizzle-kit push*)", "Bash(npx drizzle-kit migrate*)"]
  }
}

規約を守らせる3つの手段は、強制力の面で性質が違います。

3段階

止め方の違い

  • CLAUDE.md

    コンテキストとして読まれるだけの指示です。書き漏らしや誤読があり得ます。

  • permissions.deny

    指定したコマンドを、実行前に設定として拒否します。npxの有無などパターンの書き分けは必要です。

  • PreToolUseフック

    実行前にスクリプトが走り、環境変数や標準入力の中身で止めるかどうかを決められます。

リレーションと型安全なクエリ

ネストした関連データを取得する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に合わせて書かせるのが安全です。

@rcのRelational Queriesのページには、コールバック内の参照方法について「Important」と明記された制約が1つあります。テーブルのカラムはimportしたテーブルオブジェクトを直接参照せず、コールバックが渡す引数越しに参照する必要があり、これはorderByやwhere.RAW、extrasなど、コールバックを受け取るすべての句に当てはまります。@rcのwhereはオブジェクト形式で、コールバックを使うのはwhere.RAWです。

よくあるつまずき

@rcをそのまま本番に入れてしまう。PostgreSQLのGet Startedページのコマンドを疑わずコピーすると、意図せずリリース候補版がインストールされます。package.jsonのバージョン表記を都度確認する習慣が要ります。

pushをチーム共有DBに向けてしまう。ローカル検証用のつもりで実行したコマンドが、drizzle.config.tsの接続先が共有ステージング環境を向いたままだと、他の開発者のテストデータをまとめて書き換えます。接続先のURLは実行前に必ず確認します。

モデルのexport漏れ。drizzle-kitはexportされたモデルしか差分検出の対象にしません。新しいテーブルを追加してexportを忘れると、generateを実行しても何も生成されません。

列の削除が確認なしでDROPになる。スキーマからカラムを消した差分は、警告なしでDROP COLUMNのSQLになります。レビューのときDROPで検索する習慣が役に立ちます。

MCP経由のDB接続と混同する。Claude CodeにMCPサーバー経由でデータベースへ直接接続させる構成は、本記事のスキーマ管理とは別の権限設計が必要です。読み取り専用の担保方法は製品ごとに違うので、データベースMCPサーバーの読み取り専用設定を5製品で比較するで扱っている前提を先に確認しておくと、スキーマ管理とMCP接続の権限を取り違えません。

まとめ

適用前にSQLのDROPとALTER ... TYPEを目で拾う一手間が、この流れでいちばん効きます。もう1つ先に確かめたいのは、プロジェクトが安定版か@rcかです。リレーションの書き方が分かれるので、Claude Codeに書かせる前にpackage.jsonで見ておきます。

CLAUDE.mdへの規約の書き方は、Next.js SaaS MVPの構築ガイドでも同じ考え方を扱っています。AGENTS.mdとCLAUDE.mdを両方使うプロジェクトなら、AGENTS.mdとCLAUDE.mdの設定統合パターンも参考になります。バックエンド側で型を軸にした実装フローを組む発想は、Claude CodeでFastAPIバックエンドを構築するとも共通しています。

この記事を共有:XはてブLinkedIn