django-safe-migration について
django-safe-migrationは、Django開発者向けにPostgreSQLのマイグレーションをレビュー、作成、書き換えるスキルです。生成SQL、ロック動作、デプロイ方式、実行時ガードを確認し、ローリングデプロイのリスクと安全な変更方法を示します。
主な機能
- 01マイグレーションの生成SQLを確認
- 02PostgreSQLのロックと停止リスクを特定
- 03プロジェクト設定に基づき実行時ガードを評価
- 04安全なマイグレーションを作成・書き換え
- 05分割とデプロイ順序を計画
活用シーン
- 01ローリングデプロイ向けか確認
- 02NOT NULL列や外部キーを追加
- 03インデックス作成時の書き込み阻害を軽減
- 04停止を招く可能性のあるマイグレーションを修正
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vintasoftware/ django-ai-plugins --skill django-safe-migrationインストール後、Claude Code で/django-safe-migrationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/examples.mdoperation-guide.mdpostgres-locks.md詳細
Django Migration — ダウンタイムゼロ
プロジェクト設定
以下の値について、アクティブなホストのプロジェクト指示ファイル(例:AGENTS.md または
CLAUDE.md)を読みます。設定されていない場合は、表示されているデフォルト値を使用します。
| キー | デフォルト | 備考 |
|---|---|---|
| Django バージョン | unknown | db_default が利用可能かどうかに影響します。対応しているのは Django 5.0 以降のみです |
| デプロイ戦略 | rolling deploy | ローリングデプロイが最も制約の厳しい方式です。ブルーグリーンデプロイやメンテナンス時間帯のデプロイでは、より多くの操作が可能です |
| ランタイムガード | none | ガード(例:django-pg-zero-downtime-migrations)が設定されている場合、そのガードが禁止する操作はエラー、対象外の操作は警告です |
| マイグレーションコマンド | python manage.py sqlmigrate | 例:make sqlmigrate または docker compose run web python manage.py sqlmigrate |
| ドキュメント/wiki の URL | none | 設定されている場合、レビュー結果で問題を指摘する際に #<anchor> リンクを追加します |
このスキルをプロジェクト向けに設定するには、アクティブなホストのプロジェクト指示ファイルに次のようなセクションを追加します。
## django-safe-migration
- Django version: 5.1
- Deploy strategy: rolling deploy
- Runtime guard: none
- Migration command: docker compose run web python manage.py sqlmigrate
- Docs URL: https://github.com/your-org/repo/wiki/migrations
使用する場面
- 「このマイグレーションをレビューしてください」
- 「このマイグレーションは安全ですか?」
- 「次のマイグレーションを書いてください……」
- 「このマイグレーションを安全な形に書き直してください」
- 「NOT NULL 列の追加/列の削除/インデックスの追加/列名の変更/FK の追加はどうすればよいですか?」
ダウンタイムゼロのマイグレーションが重要な理由
ローリングデプロイでは、まず新しいデータベーススキーマを適用し、その後、アプリケーションのインスタンスを1つずつ再起動します。デプロイ中は常に、新旧のコードが同じデータベースに対して同時に実行されています。
以前のバージョンのアプリがトラフィックを処理している間でも、安全に実行できるマイグレーションが必要です。大きなテーブルに対して ACCESS EXCLUSIVE ロックを取得するマイグレーションは、読み取りと書き込みをすべてブロックします。負荷の高いテーブルでは、数秒の停止でもダウンタイムになります。
問題はロック自体だけではなく、待機キューにもあります。50ms で完了する ALTER TABLE でも、長時間実行中のトランザクションがあるとその後ろに並び、後続のクエリもすべてマイグレーションの後ろに並びます。負荷の高いテーブルでは、これが連鎖してコネクションプールの枯渇につながります。
PostgreSQL のロックの仕組み
| ロック | 取得する操作 | ブロックするもの |
|---|---|---|
ACCESS EXCLUSIVE | ほとんどの ALTER TABLE、DROP INDEX、DROP CONSTRAINT(FK) | すべての読み取りと書き込み |
SHARE ROW EXCLUSIVE | ADD FOREIGN KEY(子テーブルと参照先テーブルの両方で同時に取得) | 書き込みのみ(両方のテーブル) |
SHARE | CREATE INDEX | 書き込みのみ |
SHARE UPDATE EXCLUSIVE | CREATE INDEX CONCURRENTLY、VALIDATE CONSTRAINT | 実質的に何もブロックしないため、トラフィック中でも安全です |
ロックの競合マトリクス全体(テーブルレベルのロック × ビジネスロジックの操作 × 行レベルのロック)と FIFO 待機キューの説明については、references/postgres-locks.md を読み込みます。次の場合に読み込みます。
- 特定の操作がなぜ安全でないのかを説明する場合
- 開発者から、あるロックの種類が何をブロックまたは競合させるか尋ねられた場合
- 2つの同時操作が相互に影響するかを判断する場合
lock_timeout
ACCESS EXCLUSIVE を必要とする操作の前には、必ず SET LOCAL lock_timeout を指定します。これにより、ロックを無期限に待つ代わりに、マイグレーションは明確なエラーとともにすぐに失敗します。待機の連鎖によるコネクションプールの枯渇も防げます。
通常のトランザクション型マイグレーションの場合:
migrations.RunSQL("SET LOCAL lock_timeout = '2s'"),
migrations.AlterField(...), # or any ACCESS EXCLUSIVE operation
SET LOCAL はタイムアウトを現在のトランザクション内に限定するため、他のセッションには影響せず、マイグレーションの完了後も設定は残りません。
デフォルト:2s。テーブルで正当に長時間実行されるトランザクションがあるとわかっている場合は長くし、より厳格な環境では短くします。
atomic = False のマイグレーションでは、SET LOCAL と DDL を同じ RunSQL 操作にまとめます。詳しくは「構造上のルール」を参照してください。
主なパターン
NOT VALID + VALIDATE(FK および CHECK 制約向け)
長時間のロックを避けて制約を追加する、PostgreSQL の 2 段階の手法です。
ADD CONSTRAINT … NOT VALID— 制約を作成し、新しい書き込みにはただちに適用しますが、既存行のスキャンは行いません。テーブルスキャンを伴わない短時間のロックを取得します。FK 制約では両方のテーブルにSHARE ROW EXCLUSIVE、CHECK 制約ではACCESS EXCLUSIVEを取得します。VALIDATE CONSTRAINT— 既存行をスキャンし、制約を満たしていることを確認します。SHARE UPDATE EXCLUSIVEを取得し(FK 制約では参照先テーブルにROW SHAREも取得します)、読み取りや書き込みはブロックしません。
危険なのは制約の作成そのものではなく、テーブル全体のスキャンです。処理を分割することで、書き込みをブロックするロック時間を数ミリ秒に抑え、長時間のスキャンをブロックのない手順に移せます。
PostgreSQL のドキュメント:NOT VALID · VALIDATE CONSTRAINT
実行時ガード
プロジェクトによっては、マイグレーション実行時に安全でない操作に対してエラーを発生させる、カスタムのデータベースバックエンドやリンターを設定しています(例:zero_downtime_migrations、django-pg-zero-downtime-migrations、django-migration-linter)。
マイグレーションをレビューする際は、次のように扱います。
- プロジェクトの実行時ガードがブロックする操作 → エラーに分類します(マイグレーションは実行されません)
- ガードの対象外の操作 → 警告に分類します(マイグレーションは実行されますが、ダウンタイムが発生する可能性があります)
実行時ガードが設定されていない場合は、安全でない操作をすべて、プロダクションへのデプロイ前に安全な方法へ書き換える必要があるエラーとして扱います。
このプロジェクトのガードが対象とする操作については、上記の Project Configuration ブロック(または AGENTS.md)を確認してください。
モード 1:レビュー
目標
PostgreSQL でローリングデプロイを行う際に、安全でない、またはリスクのある操作をすべて特定します。
手順
- マイグレーションファイルを最後まで読みます。
<migration_command> <app_label> <migration_name>を実行し、Django が実際に実行する SQL を取得します。必ず実行してください。生成された SQL が実際の処理内容です。 アクティブなホストのプロジェクト指示ファイルにあるMigration commandの値を使用します。デフォルトはpython manage.py sqlmigrateです。このキーが設定されておらず、プロジェクトのルートにMakefileまたはdocker-compose.ymlがある場合は、ユーザーに「このプロジェクトでは sqlmigrate をどのように実行していますか?」と尋ね、その回答を指示ファイルに保存するよう提案してください。ORM の操作クラスだけでは不十分です。たとえば、FK フィールドにnull=Trueを追加するAlterFieldは、FK 制約の削除と再追加も行います。その結果、DROP CONSTRAINT(子テーブルと参照先テーブルの両方でACCESS EXCLUSIVEを取得)に続いて、NOT VALIDなしのADD CONSTRAINT FOREIGN KEY(両方のテーブルで全行スキャンを伴うSHARE ROW EXCLUSIVEを取得)が出力されます。これらはどちらもマイグレーションファイルだけからはわかりません。references/operation-guide.mdを読み込みます。検出された操作が安全でない理由を説明する際は、references/postgres-locks.mdも読み込みます。- 生成された SQL 文をそれぞれ、以下の検出チェックリストに照らして確認します。
ACCESS EXCLUSIVEを必要とする操作がある場合(エラーまたは警告として検出された場合)、レポートを出力する前にユーザーに尋ねてください。"このマイグレーションには
ACCESS EXCLUSIVE操作が含まれています。キューへの滞留やロックの連鎖を避け、すぐに失敗させるためにlock_timeoutを追加する必要があります。デフォルトは2sです。確認するか、カスタム値を指定してください。" マイグレーションにすでにSET LOCAL lock_timeoutが含まれている場合は、設定済みの値を伝え、それが適切か確認してください。 修正手順には、確認済みの値を使用します。- 次の形式で、構造化されたレポートを出力します。
マイグレーションレビュー:
エラー — ダウンタイムを引き起こす、またはランタイムガードによってブロックされます
- [ERROR]
<app>.<Model>.<field>に対する<OperationClass>:<処理内容と危険な理由> 修正:<安全な代替手段を1文で説明> この書き換えで変わること:安全なバージョンでもACCESS EXCLUSIVEが必要かどうかを明確にし、必要な場合は実際に改善される点(ロック時間(テーブル全体のスキャン → 数ミリ秒)、失敗時の動作(気づかないままキューが連鎖的に詰まる → すぐにタイムアウトエラーになる)、またはその両方)を説明してください。書き換えによってロックが完全になくなると読者に思わせないでください。 <設定されている場合は wiki/ドキュメントへのリンク>
警告 — 長時間のロックやデプロイ時の問題を引き起こす可能性があります
- [WARNING]
<OperationClass>:<問題> 修正:<安全な代替手段>
構造上の問題
- [ERROR|WARNING] <問題>(例:
atomic=Falseがない、reverseのないRunPython)
安全
- [OK] <すべてのチェックに合格した操作>
- エラーまたは警告がある場合は、書き換えを提案してください(モード3)。
- すべてのチェックに合格した場合は、マイグレーションを安全にデプロイできることを確認してください。
モード2:作成
目標
説明された変更に対して、正しく、ゼロダウンタイムのマイグレーションを生成します。
手順
references/operation-guide.mdとreferences/examples.mdを読み込みます。- 操作が曖昧な場合は確認します。
- 対象のモデルとフィールドは何ですか?
- 新しいカラムを追加しますか、それとも既存のカラムを変更しますか?
- 外部キーの場合:参照先のテーブルはどれですか?新しいカラムですか、それとも既存のカラムですか?
- 型を変更する場合:変更前と変更後の型は何ですか?
- Django のバージョンはいくつですか(
db_defaultの利用可否に影響します)?
- 操作に
ACCESS EXCLUSIVEが必要な場合は、ユーザーに確認します。"このマイグレーションでは
ACCESS EXCLUSIVEを使用します。ロックを取得できない場合にすぐ失敗するよう、lock_timeoutを設定します。デフォルトは2sです。確認するか、カスタム値を指定してください。" - 必要なマイグレーションファイルの数を判断します。多くのパターンでは、別々の PR に分けた2つのファイルが必要です。
references/examples.mdのパターンを使って、マイグレーションファイルを生成します。各ACCESS EXCLUSIVEステートメントの前にSET LOCAL lock_timeout = '<confirmed_value>'を追加してください。通常のatomic = Trueマイグレーションでは、その前にRunSQLを置けます。atomic = Falseのマイグレーションでは、DDL の実行時にもSET LOCALが有効な状態となるよう、タイムアウト設定と DDL を同じRunSQL操作にまとめてください。- 2つのファイルが必要な場合は、必ずデプロイ手順を出力してください。
## Deployment Order
- Migration 1: ships in the same PR as the model/code change
- Migration 2: ships in a follow-up PR after the deploy is confirmed stable
モード3:書き換え
目標
既存の危険なマイグレーションを、1つ以上の安全なマイグレーションに変換します。
手順
- マイグレーションファイルを読みます。
- 検出チェックリストを使って、すべての安全でない操作を特定します。
references/operation-guide.mdとreferences/examples.mdを読み込みます。ACCESS EXCLUSIVEが必要な操作がある場合は、ユーザーに確認します。"このマイグレーションには
ACCESS EXCLUSIVE操作が含まれています。ロックを取得できない場合にすぐ失敗するよう、書き換えにlock_timeoutを追加します。デフォルトは2sです。確認するか、カスタム値を指定してください。"- 安全でない操作ごとに、適切な安全パターンを適用します。
- 書き換えたマイグレーションでは、各
ACCESS EXCLUSIVE文の前にSET LOCAL lock_timeout = '<confirmed_value>'を追加します。通常のatomic = Trueのマイグレーションでは、前にRunSQLを置けます。atomic = Falseのマイグレーションでは、DDL の実行中もSET LOCALが有効になるよう、タイムアウト設定と DDL を同じRunSQL操作にまとめます。 - 書き換えにファイルの分割が必要な場合は、両方のファイルを生成し、デプロイ手順も記載します。
- 次の項目を保持します:マイグレーション番号の接頭辞、
dependencies、変更不要な安全な操作。 - 書き換え後、構造上のルール(以下を参照)に従っていることを確認します。
- マイグレーションコードの後に、必ず 「この書き換えによる変更点」 ブロックを出力し、次の内容を説明します。
ACCESS EXCLUSIVEが引き続き必要かどうか(多くの場合は必要です。明示してください)。- 実際に改善される点:ロック時間(全テーブルスキャンからメタデータのみの操作にかかる数ミリ秒へ)、失敗時の挙動(暗黙の待ち行列から
lock_timeoutによる速やかなタイムアウトエラーへ)、またはその両方。 - 導入されるデータ整合性上の猶予期間(例:Migration 2 が実行されるまで、
NOT VALIDによって既存の行は検証されません)と、テーブルの以前の状態を踏まえて問題があるかどうか。
検出チェックリスト
エラー — 実行時ガードの有無にかかわらず安全ではない操作
| Django の操作 | 検出する内容 | 安全でない理由 |
|---|---|---|
AddField | null=False で、db_default(Django 5.0 以降)がない | 古いコードはこの列を省略して挿入するため、代わりに使用できるデータベースレベルのデフォルト値がありません |
RemoveField | SeparateDatabaseAndState 内にない | 古いコードが削除された列を名前で参照し、ただちにエラーになります |
DeleteModel | SeparateDatabaseAndState 内にない | 古いコードが削除されたテーブルを参照し、ただちにエラーになります |
AddIndex | AddIndexConcurrently を使用していない | CREATE INDEX は SHARE ロックを取得し、作成中の書き込みをブロックします |
RemoveIndex | RemoveIndexConcurrently を使用していない | DROP INDEX は ACCESS EXCLUSIVE を取得し、読み取りと書き込みをブロックします |
AddConstraint(FK) | NOT VALID + VALIDATE パターンを使用していない | 子テーブルと参照先テーブルの両方で SHARE ROW EXCLUSIVE のもと、全テーブルスキャンが行われます。スキャン中は書き込み(読み取りではありません)がブロックされます |
AddConstraint(CHECK) | NOT VALID + VALIDATE パターンを使用していない | ACCESS EXCLUSIVE のもと、全テーブルスキャンが行われます |
AlterField | CHECK の手順を経ずに、既存列の null=True を解除している | ACCESS EXCLUSIVE のもと、全テーブルスキャンが行われます |
AlterField(FK フィールド) | sqlmigrate の出力に DROP CONSTRAINT に続いて ADD CONSTRAINT FOREIGN KEY が含まれている | Django は FK を削除して再追加します。DROP CONSTRAINT は子テーブルと参照先テーブルの両方で ACCESS EXCLUSIVE を取得します。また、NOT VALID を使わない再追加では、両方のテーブルで全テーブルスキャンを伴う SHARE ROW EXCLUSIVE が取得されます。SeparateDatabaseAndState と RunSQL を使って NOT VALID を追加し、lock_timeout も含めてください。 |
AddIndexConcurrently または RemoveIndexConcurrently | Migration クラスで atomic が False ではない | CONCURRENTLY はトランザクション内で実行できないため、エラーになります |
RunPython | 関数がモデルを直接インポートしている(from app.models import X) | 現在のモデルクラスを使用し、過去のマイグレーションのスナップショットを使用しないため、古いマイグレーションが動作しなくなります |
プロジェクトの実行時ガードで検出される場合の追加エラー
プロジェクトで実行時ガードが設定されている場合(「プロジェクトの設定」を参照)、これらもエラーとして報告してください(マイグレーション時に例外が発生します)。
| Django の操作 | 検出する内容 |
|---|---|
RenameField | あらゆる名前の変更 |
RenameModel | あらゆる名前の変更 |
AlterField | 列の型の変更 |
AddConstraint | ExclusionConstraint |
実行時ガードが設定されていない場合も、これらをエラーとして報告してください。安全な書き換えが必要です。
警告
| Django の操作 | 検出する内容 | 問題 |
|---|---|---|
AddConstraint(UNIQUE) | インデックスを先に作成するパターンを使用していない | SHARE ロック下でインラインインデックスを作成するため、作成中は書き込みがブロックされます |
AlterField | 既存の列に null=True を追加する(DROP NOT NULL) | ACCESS EXCLUSIVE を取得します。カタログの更新は高速ですが、長時間実行中のトランザクションがあるとその後ろに待機します。後続のクエリはすべてマイグレーションの後ろに待機するため、接続プールを枯渇させる可能性があります |
RunPython | reverse_code がない | 元に戻せません |
RunSQL | reverse_sql がない | 元に戻せません |
構造上のルール(常に確認)
- いずれかの操作で
CONCURRENTLYまたはVALIDATE CONSTRAINTを使用する場合は、Migrationクラスでatomic = Falseを指定します。VALIDATE CONSTRAINTについては、特に次の点に注意してください。(1)SHARE UPDATE EXCLUSIVEは同じロックとの競合を引き起こします。references/postgres-locks.mdの競合マトリックスを参照してください。(2)atomic = Trueにすると、全スキャンの実行中、ラッパートランザクションが開いたままになり、それ以前のすべてのステートメントのロックを保持して、デッドロックのリスクが高まります。(3)atomic = Falseにすると、VALIDATE は独自のトランザクションで実行され、完了時にロックがすぐに解放されます。 RemoveField/DeleteModelにはSeparateDatabaseAndStateによる分割を使用し、必ず別々の 2 つのファイルにします。RunPythonでは、最低限reverse_code=migrations.RunPython.noopを必ず指定します。RunSQLでは、必ずreverse_sqlを指定します。本当に元に戻せない場合は、migrations.RunSQL.noopを使用し、その理由を記載します。RunPythonでモデルにアクセスする場合は、必ずapps.get_model("app", "ModelName")を使用し、直接インポートしないでください。ACCESS EXCLUSIVEを取得する操作の前には、必ずSET LOCAL lock_timeoutを設定します。デフォルト値は2sです。作成または書き換えを行う前に、必ずユーザーに確認してください。タイムアウトを現在のトランザクション内に限定するため、SETではなくSET LOCALを使用します。atomic = Falseの例外:SET LOCALはトランザクションの終了時にのみリセットされます。atomic = Falseのマイグレーションでは、各操作はラッパートランザクションの外で実行されるため、単独のmigrations.RunSQL("SET LOCAL lock_timeout = '2s'")は次の操作の実行前にリセットされ、効果がありません。マイグレーションでatomic = Falseを指定する場合は、タイムアウト設定と DDL を必ず 1 つのRunSQL呼び出しにまとめてください。migrations.RunSQL(""" SET LOCAL lock_timeout = '2s'; ALTER TABLE app_model ADD COLUMN ...; """)