Supabase でローカル開発をしていると、スキーマを変更するたびにマイグレーションファイルを作って、ローカル DB にも適用して…という作業が地味に面倒ですよね。私も以前は Supabase CLI だけで開発していましたが、Drizzle ORM に移行してからローカル開発がびっくりするほど快適になりました。
この記事では、Drizzle ORM の「コードファースト」アプローチがなぜ便利なのか、そしてpush・generate・migrateという 3 つのコマンドをどう使い分けるのかを、実体験をもとに紹介します。
Supabase CLIだけのローカル開発、つらくないですか?
Supabase CLI でローカル開発環境を構築すると、こんな流れになります。
supabase migration newでマイグレーションファイルを作成- SQL を手書きする
supabase db resetでローカル DB をリセット&マイグレーション適用- 本番にも同じマイグレーションを適用
ちょっとしたカラム追加でも、毎回この手順を踏む必要があります。開発初期でスキーマが頻繁に変わる時期だと、正直かなり煩わしいんですよね。
「TypeScript でスキーマを定義して、それがそのままローカル DB に反映されたら楽なのに…」と思っていたところ、Drizzle ORM がまさにそれを実現してくれました。
Drizzleの「コードファースト」とは
ORM のアプローチには大きく 2 つあります。
| アプローチ | 真実のソース | 特徴 |
|---|---|---|
| データベースファースト | データベーススキーマ | DBを先に設計し、そこからコードを生成 |
| コードファースト | TypeScriptコード | コードでスキーマを定義し、DBに反映 |
flowchart TB
subgraph DB["データベースファースト"]
direction TB
D1[DBスキーマ設計] --> D2[SQL作成]
D2 --> D3[コード生成]
end
subgraph CF["コードファースト"]
direction TB
C1[TypeScriptスキーマ定義] --> C2[drizzle-kit push/migrate]
C2 --> C3[DB自動更新]
endDrizzle ORM はどちらのアプローチにも対応していますが、コードファーストを使うと開発体験が大きく変わります。
コードファーストの流れ
// schema.ts - これが真実のソース
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
この TypeScript ファイルを変更すれば、それがそのままデータベースの定義になります。SQL を手書きする必要はありません。
3つのアプローチ: push、generate、migrate
Drizzle ORM には、スキーマをデータベースに反映する 3 つの方法があります。これが最初はちょっとわかりにくいので、整理しておきます。
| コマンド | 用途 | マイグレーションファイル |
|---|---|---|
drizzle-kit push | 開発環境での高速反映 | 生成しない |
drizzle-kit generate | マイグレーションファイル生成 | 生成する |
drizzle-kit migrate | マイグレーション適用 | 適用する |
push: ローカル開発の救世主
drizzle-kit pushは、スキーマの変更を直接データベースに適用するコマンドです。マイグレーションファイルを生成せず、差分を検出してその場で SQL を実行します。
npx drizzle-kit push
これを実行すると、内部では以下の処理が行われます。
- スキーマファイルを読み込んでスナップショット化
- 現在のデータベーススキーマを取得
- 差分を検出して SQL を生成
- その SQL を即座に実行
ローカル開発では、スキーマを変更したらpushするだけ。マイグレーションファイルの管理は不要です。これがめちゃくちゃ快適なんです。
generate + migrate: 本番環境向け
本番環境では、マイグレーションファイルを Git で管理して、デプロイ時に適用する流れが一般的です。
# 1. マイグレーションファイルを生成
npx drizzle-kit generate
# 2. マイグレーションを適用
npx drizzle-kit migrate
generateを実行すると、以下のようなファイルが生成されます。
📂 drizzle
└ 📂 0001_add_users_table
├ 📜 migration.sql
└ 📜 snapshot.json
migration.sqlには実際に実行される SQL、snapshot.jsonにはスキーマのスナップショットが含まれます。これを Git にコミットしておけば、チームメンバーや本番環境で同じマイグレーションを再現できます。
使い分けの目安
| シーン | 推奨アプローチ |
|---|---|
| ローカル開発(スキーマ試行錯誤中) | push |
| ローカル開発(安定期) | generate + migrate |
| 本番環境 | generate + migrate |
| チーム開発 | generate + migrate |
個人的には、開発初期はpushでガンガン試して、スキーマが固まってきたらgenerateに切り替えるのがおすすめです。
Supabase × Drizzleのセットアップ
実際に Supabase プロジェクトで Drizzle を使う設定を見ていきましょう。
パッケージのインストール
npm install drizzle-orm postgres
npm install -D drizzle-kit
環境変数の設定
# .env
DATABASE_URL=postgresql://postgres:password@localhost:54322/postgres
Supabase Local を使っている場合、デフォルトのポートは 54322 です。
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!,
},
});
スキーマ定義
// src/db/schema.ts
import { pgTable, uuid, text, timestamp, decimal, integer } from 'drizzle-orm/pg-core';
export const products = pgTable('products', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
description: text('description'),
price: decimal('price', { precision: 10, scale: 2 }).notNull(),
stock: integer('stock').default(0),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
データベース接続
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
ローカル開発がこう変わる
Before: Supabase CLIのみ
# 1. マイグレーションファイルを作成
supabase migration new add_products_table
# 2. SQLを手書き(面倒...)
# supabase/migrations/xxx_add_products_table.sql を編集
# 3. ローカルDBに適用
supabase db reset
# 4. 「あ、カラム追加したい」→ 1に戻る
After: Drizzle + push
# 1. schema.tsを編集(TypeScriptで快適)
# 2. ローカルDBに反映
npx drizzle-kit push
# 3. 「カラム追加したい」→ schema.ts編集 → push
スキーマ変更のサイクルが圧倒的に速くなります。SQL を書く必要がないし、マイグレーションファイルの管理も不要。TypeScript の型補完も効くので、タイポも減ります。
本番環境へのマイグレーション戦略
ローカルではpushで開発して、本番にはgenerate + migrateでデプロイする。これが基本戦略です。
package.jsonのスクリプト
{
"scripts": {
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}
本番環境での注意点
海外の事例を調べると、Railway や Vercel で Drizzle を本番運用する際の Tips がいくつかありました。
SSL設定はrequireを使う
Railway などのホスティングサービスでは、SSL 接続が必須です。接続文字列に?sslmode=requireを追加するか、接続オプションで指定します。
const client = postgres(process.env.DATABASE_URL!, {
ssl: 'require', // verify-fullではなくrequire
});
自己署名の証明書を使うサービスでは、verify-full だとエラーになる場合があります。
接続プールは控えめに
const client = postgres(process.env.DATABASE_URL!, {
max: 1, // 本番では接続数を制限
});
サーバーレス環境では、接続プールのサイズを小さくしておくと安定します。
デプロイ時にマイグレーションを実行
Docker を使う場合は、起動スクリプトでマイグレーションを実行してからアプリを起動する構成が一般的です。
{
"scripts": {
"start": "npm run db:migrate && node dist/index.js"
}
}
Supabaseのマイグレーションフォルダと連携する
「Supabase CLI のマイグレーションも使いたい」という場合は、Drizzle の出力先を Supabase のフォルダに向けるテクニックがあります。
// drizzle.config.ts
export default defineConfig({
schema: './src/db/schema.ts',
out: './supabase/migrations', // Supabaseのフォルダに出力
dialect: 'postgresql',
// ...
});
こうすると、drizzle-kit generateで生成されたマイグレーションがsupabase/migrationsに保存され、supabase db pushで Supabase に反映できます。ただし個人的には、Drizzle 単体で完結させる方がシンプルでおすすめです。
まとめ
Drizzle ORM のコードファーストアプローチを使うと、Supabase のローカル開発が格段に快適になります。
- 開発時は
push- スキーマ変更を即座にローカル DB に反映 - 本番は
generate+migrate- マイグレーションファイルを Git 管理
TypeScript でスキーマを定義して、drizzle-kit pushするだけ。SQL を手書きする必要も、マイグレーションファイルを管理する必要もありません。
Supabase CLI のマイグレーション管理に疲れている方は、ぜひ Drizzle ORM を試してみてください。
API設計についてより深く知りたい方は、以下の記事も参考にしてみてください。
API設計アンチパターン7選 - UI都合の設計が招く技術的負債開発