この記事は、ひとりでつくるSaaS - 設計・実装・運用の記録 Advent Calendar 2025 の8日目の記事です。
昨日の記事では「データベースのID設計」について書きました。この記事では、DBマイグレーションの運用方法について解説します。
この記事で紹介する方法は、試行錯誤の末にたどり着いた独自のやり方です。もっと良い方法があれば、ぜひコメントで教えてください。
🎯 一般的なマイグレーション管理の方法
DBマイグレーションには、いくつかの管理方法があります。
ORMのマイグレーション機能を使う
Drizzle ORM、Prisma、TypeORMなどのORMには、マイグレーション機能が組み込まれています。
# Drizzle ORMの場合
npx drizzle-kit generate # スキーマからマイグレーションを生成
npx drizzle-kit migrate # マイグレーションを適用
たとえば、TypeScriptでテーブル定義を書くと、その変更を検出して ALTER TABLE などのSQLを自動生成してくれます。
メリット:
- コードの変更から自動でマイグレーションSQLを生成
- 適用履歴をDBのテーブルで管理
- コマンド一発で適用できる
課題:
- 複雑なデータ移行(既存データの変換など)には対応しにくい
- 何が実行されるか把握しづらいことがある
複数環境を管理する場合の課題
ORMのマイグレーション機能は単一環境の管理には便利ですが、開発環境と本番環境を分けて運用する場合、以下のような課題があります。
- 開発環境と本番環境でスキーマが合わなくなる
- 「開発で適用したが本番にはまだ」という状態が把握しづらい
- 何をいつ適用したか分からなくなる
私が開発しているMemoreruでも同じ課題に直面し、試行錯誤の結果、現在のような運用ルールにたどり着きました。
📂 個人開発プロダクトでのマイグレーション管理
連番ファイルで時系列管理
マイグレーションファイルはClaude Codeに作成してもらい、連番で管理しています。
database/migrations/
├── sql/
│ ├── 001_create_users_table.sql
│ ├── 002_create_posts_table.sql
│ ├── 003_add_user_profile.sql
│ ├── 004_add_status_column.sql
│ └── ...
├── scripts/
│ └── migrate.sh
├── status.json
└── README.md
連番管理のメリット:
- 適用順序が一目で分かる
- ファイル名でどの時点のスキーマか分かる
- 本番と開発の差分を把握しやすい
SQLファイルを直接管理する理由
Drizzle ORMのマイグレーション生成機能(drizzle-kit generate)は使わず、SQLファイルを直接作成しています。ただし、スキーマ定義自体はDrizzle ORMで管理しているため、型安全性は保たれています。
SQLファイルを直接管理する理由:
- 複雑な変更(データ移行を伴うもの)に対応しやすい
- 何が実行されるか完全に把握できる
- トラブル時の原因特定が容易
🔄 開発・本番共通のマイグレーションスクリプト
Memoreruでは、開発環境と本番環境で同じスクリプトを使ってマイグレーションを適用しています。
なぜ共通スクリプトか
# 開発環境
./database/migrations/scripts/migrate.sh dev 004_add_status_column.sql
# 本番環境
./database/migrations/scripts/migrate.sh pro 004_add_status_column.sql
共通スクリプトのメリット:
- リハーサル効果: 開発環境で本番と同じ手順を踏むことで、本番適用前に問題を発見できる
- 手順の統一: 開発はClaude Codeで直接実行、本番はスクリプト...という違いがあると事故のもと
- ログの一元管理: 両環境の実行ログが同じ形式で残る
環境別の違い
| 項目 | 開発環境 | 本番環境 |
|---|---|---|
| 接続情報 |
.env.localから自動読み込み |
毎回手動入力 |
| バックアップ推奨 | なし | 警告表示 |
本番環境の接続文字列を毎回入力するのは手間ですが、これが安全策として機能します。誤って開発環境のつもりで本番を操作する事故を防げます。
また、Claude Codeには本番環境のDB接続情報を教えていません。これにより、AIが誤って本番DBを操作するリスクを排除しています。
なお、どちらの環境でもマイグレーション適用前にpgAdminのバックアップ機能でバックアップを取得しています。万が一のロールバックに備えておくことが大切です。
🛡️ 安全に適用するための仕組み
スクリプトには以下のような安全策を組み込んでいます。
- 確認フロー: 適用前に確認プロンプトを表示し、誤操作を防ぐ
- 接続テスト: 適用前にDBへの接続を確認
-
ログの自動保存: すべての実行ログを
logs/migrations/に保存し、後から確認できるようにする
スクリプトの具体的な実装はClaude Codeに作成してもらいました。要件を伝えれば、環境に合わせたスクリプトを生成してくれます。
📊 status.jsonで適用状況を一元管理
開発環境と本番環境の適用状況を1つのファイルで管理しています。
{
"lastUpdated": "2025-12-04",
"environments": {
"dev": {
"name": "開発環境",
"lastApplied": "004_add_status_column",
"appliedAt": "2025-12-04"
},
"pro": {
"name": "本番環境",
"lastApplied": "003_add_user_profile",
"appliedAt": "2025-11-30"
}
},
"pending": {
"pro": ["004_add_status_column"]
}
}
本番未適用の確認
# pendingの一覧を表示
jq '.pending.pro' database/migrations/status.json
# => ["004_add_status_column"]
開発環境で適用したマイグレーションのうち、本番にまだ適用していないものが一目で分かります。
自動更新
マイグレーション適用後、スクリプトが自動的に status.json を更新します。手動で更新する必要がないため、更新忘れを防げます。
💡 実践Tips
Tip 1: 破壊的変更は段階的に
カラム名の変更やテーブル構造の変更は、一度に行わず段階的に実行します。
-- ステップ1: 新しいカラムを追加
ALTER TABLE contents ADD COLUMN new_name TEXT;
-- ステップ2: データを移行
UPDATE contents SET new_name = old_name;
-- ステップ3: 古いカラムを削除(別のマイグレーションで)
ALTER TABLE contents DROP COLUMN old_name;
ステップ2と3の間にアプリケーションの動作確認を挟むことで、問題があっても影響を最小限にできます。
Tip 2: ロールバック用SQLも用意
重要なマイグレーションには、ロールバック用のSQLもコメントで残しておきます。
-- マイグレーション
ALTER TABLE contents ADD COLUMN status TEXT DEFAULT 'draft';
-- ロールバック(必要時のみ実行)
-- ALTER TABLE contents DROP COLUMN status;
Tip 3: Claude Codeとの協働ルール
CLAUDE.mdにマイグレーション運用のルールを明記しています。
## マイグレーション運用
- 直接psqlでSQLを実行しない
- 必ずmigrate.shスクリプト経由で適用
- 本番適用前に開発環境でリハーサル
- 適用後はstatus.jsonをコミット
AIエージェントが誤って直接SQLを実行することを防いでいます。
✅ まとめ
DBマイグレーション運用から得た学びをまとめます。
うまくいっていること:
- 連番ファイルで時系列管理
- 開発・本番共通スクリプトでリハーサル
- status.jsonで適用状況を一元管理
- 確認フローで誤操作防止
注意が必要なこと:
- 手動SQL管理は変更量が増えると大変になる可能性
- 複雑なデータ移行は事前にテストデータで検証
- ロールバック手順も事前に考えておく
個人開発でも、最初からルールを決めておくことで、後から困ることが少なくなります。
明日は「NextAuth.jsからBetter Authへ:認証ライブラリを移行した理由」について解説します。
シリーズの他の記事
- 12/7: データベースのID設計:ID方式の選択と主キーの考え方
- 12/9: NextAuth.jsからBetter Authへ:認証ライブラリを移行した理由