Drizzle ORMのコードファーストでSupabaseローカル開発が劇的に楽になった話

開発

Supabase でローカル開発をしていると、スキーマを変更するたびにマイグレーションファイルを作って、ローカル DB にも適用して…という作業が地味に面倒ですよね。私も以前は Supabase CLI だけで開発していましたが、Drizzle ORM に移行してからローカル開発がびっくりするほど快適になりました。

この記事では、Drizzle ORM の「コードファースト」アプローチがなぜ便利なのか、そしてpushgeneratemigrateという 3 つのコマンドをどう使い分けるのかを、実体験をもとに紹介します。

Supabase CLIだけのローカル開発、つらくないですか?

Supabase CLI でローカル開発環境を構築すると、こんな流れになります。

  1. supabase migration newでマイグレーションファイルを作成
  2. SQL を手書きする
  3. supabase db resetでローカル DB をリセット&マイグレーション適用
  4. 本番にも同じマイグレーションを適用

ちょっとしたカラム追加でも、毎回この手順を踏む必要があります。開発初期でスキーマが頻繁に変わる時期だと、正直かなり煩わしいんですよね。

「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自動更新]
    end

Drizzle 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

これを実行すると、内部では以下の処理が行われます。

  1. スキーマファイルを読み込んでスナップショット化
  2. 現在のデータベーススキーマを取得
  3. 差分を検出して SQL を生成
  4. その 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都合の設計が招く技術的負債開発
Thanks for reading!
Drizzle ORMSupabaseTypeScriptORMマイグレーション