rails-migrations について
rails-migrationsは、Railsのスキーマ変更とデータ移行を安全に計画・レビューするスキルです。可逆な変更、段階的な展開、リスクの高いバックフィルをデプロイ時の移行から切り離す方法を扱います。
主な機能
- 01可逆なマイグレーションの指針
- 02安全なスキーマとインデックス変更
- 03スクリプトによるバッチバックフィル
- 04マルチテナント向けインデックス戦略
- 05段階的な展開手順
活用シーン
- 01マイグレーションのロックリスク確認
- 02大規模なデータバックフィルの計画
- 03複数回のデプロイにわたるカラム置換
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ marckohlbrugge/ 37signals-skills --skill rails-migrationsインストール後、Claude Code で/rails-migrationsを入力して読み込まれたか確認します。
詳細
Rails のマイグレーション
スキーマの変更とマイグレーションの安全性に使用します。
ルール
- 可能な限り、マイグレーションを元に戻せるようにします(データ移行には、
up/downの SQL を明示したreversibleブロックを使用します)。 - マイグレーション内でのデータ操作には生の SQL を使用し、時間の経過とともに変化するアプリケーションモデルへの参照は避けます。
- 大規模なテーブルで長時間ロックすることを避け、リスクのある作業は、デプロイに安全な複数の段階に分けます。
- スキーマの変更と大規模なデータのバックフィルを分けます。
- 再実行しても安全なデータベース操作を優先します(マイグレーションがさまざまな状態で実行される可能性がある場合は、
table_exists?/column_exists?のガードを使用します)。 - 小規模なインラインバックフィルなら、1 つのマイグレーションで問題ありません(カラム追加 →
execute "UPDATE ..."→ 制約の厳格化)。時間のかかる処理は、マイグレーションの実行経路から完全に切り離します。
スクリプトによるバックフィル(すべてが db:migrate ではありません)
時間のかかるバックフィルやリスクのあるデータバックフィルは、script/migrations/*.rb に置き、手動で実行します(例:Kamal 経由)。デプロイ時のマイグレーション実行時間内には含めません。
- ヘッダーコメントに前提条件と実行手順を記載します。
- 事前確認クエリで、変更前に対象範囲を表示します。
- 冪等にします。すでに処理された行をスキップし(
next if Entry.exists?(...))、安全に再実行できるようにします。 find_each/in_batchesでバッチ処理し、巨大な書き込みを一度に行わないようにします。
安全なパターン
- NULL 許可のカラムを追加 → バックフィル →
NOT NULLを適用。 - 必要に応じて、サポートされている場合はインデックスを並行して追加します。
- データを重複排除し(古い重複データを生の SQL で削除)、同じマイグレーション内で、ユニークインデックスを追加する直前に実行します。
- データのみのマイグレーションも適切です。
upではfind_each+update!、downではupdate_allを使用します。 - 巨大な書き込みを一度に行うのではなく、バッチ処理でバックフィルします。
制約:デフォルトではなく、意図して選択するもの
- AR の
validates uniquenessより、DB レベルの一意性インデックスを優先します(バリデーションには競合状態が発生しますが、インデックスでは発生しません)。 - 外部キーはトレードオフです。Fizzy では DDL の高速化とシャーディングへの適合性を優先し、外部キーをすべて意図的に削除しています(参照には
foreign_key: falseを指定)。整合性は Rails で維持します。どちらの方針でも構いませんが、一貫性を保ち、文書化してください。偶然そうなった状態にしないでください。 - CHECK 制約の使用が難しい場合は、コストの低いスキーマ上の工夫で、特殊な不変条件を適用します。例:
singleton_guardカラムのデフォルト値を 0 にし、ユニークインデックスを設定して、シングルトンテーブルを実現します。 - テナントごとのカウンターをアトミックに更新します。連番には
account.increment!(:cards_count)と一意な[account_id, number]インデックスを使用します。グローバル ID の連番には、ロック下でのfirst_or_create!とincrement!を使った、ロックされた連番行を使用します。
マルチテナントのインデックス戦略
- アプリケーションをマルチテナント化する際は、専用のマイグレーションフェーズでグローバルインデックスを
[account_id, ...]の複合インデックスに置き換えます。不要になった単一カラムインデックスを削除し、削除理由をコメントで説明します。 - スコープ付きの一意性は DB レベルで適用します。
add_index :tags, [:account_id, :title], unique: true。
複数データベース/複数アダプターのアプリケーション
- Solid Queue/Cache/Cable には、それぞれ個別のデータベースと
migrations_paths、スキーマファイルを用意します。 - 1 つのコードベースで SQLite と MySQL をサポートする場合:マイグレーション内でアダプターを早期に判定して処理を戻し(
return if connection.adapter_name == "SQLite")、アダプター固有の DDL(FTS5 とシャーディングされた全文検索など)を使用し、アダプターごとのschema_dumpを使ってスキーマダンプを分けます(schema.rb/schema_sqlite.rb)。 - 本番環境で SQLite を使用する場合は、同時書き込み時の
SQLITE_BUSYを減らすためにdefault_transaction_mode: immediateを設定します。 - 本番環境では
dump_schema_after_migrationを無効にします。
段階的なロールアウト手順
-
カラムの置き換え
- デプロイ 1:新しい NULL 許可カラムを追加します。
- デプロイ 2:二重書き込み/バックフィルを行います。
- デプロイ 3:新しいカラムから読み取るようにします。
- デプロイ 4:制約を適用し、その後、古いカラムを削除します。
-
制約の強化
- まずデータのクリーンアップ/バックフィルを行います。
- データが準拠した後にのみ、インデックス/制約を追加します。
- 制約が適用されたら、それに依存するようにアプリケーションの動作を切り替えます。
-
破壊的な変更
- まずアプリケーションコードでの読み書きを非推奨にします。
- 別のデプロイで使用を取り除きます。
- 確認期間を経てから、カラム/テーブルを削除します。
注意すべき点
- 明確な理由のない、元に戻せないマイグレーション。
- スキーマの書き換えと大規模なデータマイグレーションを一度に行うこと。
- トランザクションの負荷が高いデフォルトのマイグレーション内で大規模なバックフィルを行うこと(
script/migrations/に移してください)。 - 段階的な非推奨化を行わずにカラムやテーブルを削除すること。
- マイグレーション内でアプリケーションのモデルを参照すること(モデルの動作は変化するため、SQL または最小限のインライン AR クラスを使用してください)。
- 既存データの重複を解消せずに一意インデックスを追加すること。
- 裏付けとなる一意インデックスがない
validates uniqueness。
コメント
Markdown 対応