Claude Media
Claude CodeでEF Coreのマイグレーションを本番適用前に洗う手順

Claude CodeでEF Coreのマイグレーションを本番適用前に洗う手順

dotnet ef migrations scriptで出したSQLをclaude -pに読ませ、列の削除・型変更・既定値なしのNOT NULL追加を適用前に拾う手順を、JSON出力とCIの止め方まで書きます。

Claude CodeでEF Coreのマイグレーションを本番適用前に洗う手順

EF Coreのマイグレーションは、C#のコードを読んでもテーブルに何が起きるかが見えにくいのが難点です。migrationBuilder.DropColumnの1行が、本番の1列分のデータを消します。

そこで、読む対象をC#ではなくdotnet ef migrations scriptが出すSQLに替え、claude -pに危険な文を拾わせます。この記事では、スクリプトの作り方、拾わせる観点、JSONで受け取ってCIで止める流れ、そしてこの方法で見えないものまでを順に書きます。

C#ではなくSQLを読ませる理由

Microsoftのドキュメントは、デプロイの前に生成SQLを確認・修正したい場合の選択肢としてSQLスクリプトを挙げています。スクリプトはレビューでき、CIで生成でき、DBAに渡して別に保管もできる、という位置づけです。

対比として、マイグレーションバンドルは自動デプロイ向きですが、実行するSQLを事前に見る手段がありません。dotnet ef database updateも、ツールがSQLを直接適用するため、内容を確認する機会が挟まりません。

適用方法実行前にSQLを確認できるかレビュー対象にしやすいか
SQLスクリプト実行前にSQLを確認できるかできるレビュー対象にしやすいか向く
マイグレーションバンドル実行前にSQLを確認できるかできないレビュー対象にしやすいか向かない
dotnet ef database update実行前にSQLを確認できるかできないレビュー対象にしやすいか向かない

つまり、AIに読ませる対象としても、人のレビューと同じくSQLスクリプトが素直な入口です。C#のマイグレーションクラスはUpとDown、スナップショット、Designerファイルに分かれていて、「結局どのDDLが流れるか」を組み立てる作業が要ります。その組み立てをdotnet efに任せ、結果のSQLだけを渡します。

読ませるSQLを2通り作る

dotnet ef migrations scriptは、出力先を指定しなければ標準出力に書きます。--output(短縮形-o)でファイルにできます。

# 全マイグレーションを含む、どのバージョンのDBにも流せるスクリプト
dotnet ef migrations script --idempotent \
  --output artifacts/migrations.sql
 
# 本番に適用済みの最新から、今回の範囲だけ
dotnet ef migrations script 20260915093000_AddOrderNote \
  --output artifacts/pending.sql

--idempotent(-i)は、__EFMigrationsHistoryテーブルで適用済みかを確かめながら、未適用のマイグレーションだけを実行するスクリプトを作ります。どのマイグレーションまで適用されているか分からないDBや、バージョンの異なる複数のDBへ配るときに使えます。ただし対応はプロバイダー次第で、たとえばSQLiteは対応していません。

2つ目の書式は、<FROM>に「本番DBに最後に適用済みのマイグレーション」を渡します。<TO>を省くとプロジェクトの最新までです。FROMに0を渡すと、最初のマイグレーションより前からの全文になります。

役割は次のように分けると扱いやすくなります。

  • 配布用: --idempotentのスクリプトをそのままデプロイ成果物にする
  • レビュー用: FROM指定の範囲を読ませる

idempotentのスクリプトには過去分のマイグレーションもすべて入っています。過去に適用済みの履歴まで毎回読ませると、指摘が古い変更で埋まります。レビュー用に範囲を絞るのはそのためです。

Visual Studioのパッケージマネージャーコンソールでは、Script-Migration -Idempotent -Output artifacts\migrations.sqlが同じ働きをします。

危険な変更の見分け方

ClaudeにSQLを渡す前に、何を危険と呼ぶかを決めておきます。EF Core固有の事情は、ドキュメントに次のように書かれています。

  • プロパティの名前をNameからFullNameに変えると、EF CoreはDropColumnとAddColumnを生成する。そのまま適用すると、顧客名がすべて失われる
  • 意図が改名なら、RenameColumnに書き換える必要がある
  • データ損失の可能性がある操作(列の削除など)があると、マイグレーションの作成時に警告が出る

この「改名のつもりが削除と追加になる」は、SQLにするとDROP COLUMNとADDが同じテーブルに並ぶ形で現れます。CLIの警告を見落としたままコミットされた場合の、最後の網になります。

拾わせる観点を、次の表にまとめます。表の右列はこの記事で決めた判定ルールで、プロンプトにそのまま写します。

SQLの形何が起きうるか判定
DROP COLUMN / DROP TABLE何が起きうるかデータが消える。同じテーブルに同名に近いADDがあれば改名の取り違え判定high
ALTER COLUMNで型や長さを変更何が起きうるか変換できない値で失敗、または値が切り詰められる判定high
既存テーブルへのNOT NULL列追加でDEFAULTなし何が起きうるか既存行がある表では失敗、または意図しない値が入る判定high
NULL許容列をNOT NULLへ変更何が起きうるかNULLの行が残っていれば失敗する判定high
CREATE INDEX・ADD CONSTRAINT何が起きうるか大きな表では時間がかかり、その間の更新を待たせることがある判定medium
CREATE TABLE・NULL許容列の追加何が起きうるか既存データに影響しない判定low

mediumの行は、SQLの文面だけでは判断が付きません。表の行数やデータベースの種類・エディションで、待ち時間が桁違いに変わるからです。この観点は、あとの限界の節で触れます。

claude -pに読ませてJSONで受け取る

非対話モードのclaude -pは標準入力を読めます。ログやdiffをパイプで渡す使い方が、公式のヘッドレス実行の基本形です。パイプで渡す利点は、ファイルを読むためのツール許可さえ要らないことです。

cat artifacts/pending.sql | claude --bare -p \
  "このEF CoreのSQLスクリプトを適用前レビューする。..." \
  --output-format json \
  --json-schema "$(cat review-schema.json)" \
  | jq '.structured_output'

ポイントは3つあります。

  • --bare: CIで毎回同じ結果にするため、ローカルのhooksやCLAUDE.md、自動メモリーの自動読み込みを切る。ログインではなくANTHROPIC_API_KEYで認証する
  • --output-format jsonと--json-schema: スキーマに沿った結果がstructured_outputフィールドに入る
  • 標準入力の上限は10MB。超えると非ゼロの終了コードで止まる

--json-schemaの値は、ファイルから$(cat ...)で渡すと長いスキーマを読みやすく保てます。review-schema.jsonの例です。

{
  "type": "object",
  "properties": {
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "statement": {"type": "string"},
          "table": {"type": "string"},
          "severity": {"enum": ["high", "medium", "low"]},
          "reason": {"type": "string"},
          "suggestion": {"type": "string"}
        },
        "required": ["statement", "table", "severity", "reason"]
      }
    }
  },
  "required": ["findings"]
}

プロンプト本体は、判定ルールを表と同じ中身で渡します。たとえば次のように書きます。

標準入力は dotnet ef migrations script の出力(SQL Server向け)。
次のルールで文ごとに判定し、該当した文だけ findings に入れる。
- DROP COLUMN / DROP TABLE は high。同じテーブルに ADD COLUMN が
  並ぶなら、改名の取り違えの可能性を reason に書く
- ALTER COLUMN による型・長さの変更は high
- 既存テーブルへの NOT NULL 列追加で DEFAULT が無ければ high
- NULL許容から NOT NULL への変更は high
- CREATE INDEX と ADD CONSTRAINT は medium
- 該当なしなら findings は空配列にする
SQLに書かれていない事実(行数、ロックの長さ)は推測しない。

最後の1行が効きます。SQLの文面から言えること以外を断定させないと、「おそらく数分ロックされます」のような根拠のない見立てが混ざります。

返ってくるJSONは、たとえば次のような形になります(スキーマに沿った例示であり、実際の指摘内容はスクリプトごとに変わります)。

{
  "findings": [
    {
      "statement": "ALTER TABLE [Customers] DROP COLUMN [Name];",
      "table": "Customers",
      "severity": "high",
      "reason": "列の削除。同じ表に FullName の追加があり、改名の取り違えの可能性",
      "suggestion": "RenameColumn に書き換える"
    }
  ]
}

highがあればCIを落とす

結果が構造化されているので、シェルで判定できます。jq -eは、式の結果がfalseかnullのとき非ゼロで終了します。

cat artifacts/pending.sql | claude --bare -p "$(cat review-prompt.txt)" \
  --output-format json \
  --json-schema "$(cat review-schema.json)" > review.json
 
jq -r '.structured_output.findings[]
  | "[\(.severity)] \(.table): \(.reason)"' review.json
 
jq -e '[.structured_output.findings[]
  | select(.severity == "high")] | length == 0' review.json

structured_outputは、実行が有効な構造化出力を返せなかったとき、レスポンスに含まれません。そのままだとjqがnullを踏んで動作が曖昧になるため、has("structured_output")を先に確かめるガードを1行足しておくと安全です。

CI側では、ヘッドレス実行の呼び方をGitHub Actionsの引数まとめと合わせて決めます。ログのような大きな入力を扱うときの10MB上限は、パイプ入力の10MB制限の記事にまとめています。

スクリプトを作る環境を固定する

dotnet efは、DbContextを組み立てるためにアプリケーションのコードを実行します。プロバイダーの選択、接続文字列、モデルの設定が、実行時の環境に左右されるということです。

環境変数ASPNETCORE_ENVIRONMENTもDOTNET_ENVIRONMENTも設定されていないときは、設計時ツールはDevelopment環境として動きます。開発用の設定でプロバイダーが変わると、スクリプトのSQLの方言まで変わりえます。たとえば開発はSQLite、本番はSQL Serverという構成では、レビューしたSQLが本番向けではありません。

デプロイ用のスクリプトを作るときは、環境を明示します。

ASPNETCORE_ENVIRONMENT=Production \
  dotnet ef migrations script --idempotent \
  --output artifacts/migrations.sql

この環境指定は、レビューに回すSQLと本番に流すSQLを一致させるためのものです。スクリプトの生成はCIで1回だけ行い、同じファイルを「Claudeに読ませる」と「デプロイに使う」の両方に渡すと、読んだものと流すものが食い違いません。

あわせて、マイグレーションを実際に適用するIDには、スキーマ変更の権限を持つ別のIDを使い、アプリの実行時のIDにはデータの読み書きに必要な権限だけを持たせます。接続文字列はソース管理に置かず、デプロイ側の秘密情報ストアから渡します。この点はClaude Codeの手元に本番の接続情報を置かない理由と同じです。

手元で使うためのCLAUDE.mdと権限

手元でClaude Codeに対話的に直させるときは、CLAUDE.mdに判定ルールを書いておくと、同じ観点で毎回読ませられます。

## EF Coreのマイグレーション
- マイグレーションを追加したら `dotnet ef migrations script <適用済みの最新>`
  を実行し、出力のSQLを読んでから終える
- DROP COLUMN、型変更、既定値なしのNOT NULL追加が出たら、
  改名やデータ移行が必要でないかをユーザーに確認する
- `dotnet ef database update` は実行しない(適用は人が行う)

最後の1行は、権限の側でも固定できます。dotnet ef migrations scriptは読み取りの扱いにとどめ、データベースを書き換えるdatabase updateは確認に回します。

{
  "permissions": {
    "allow": ["Bash(dotnet ef migrations script *)"],
    "ask": ["Bash(dotnet ef database update *)"]
  }
}

指摘されたときの直し方

典型は、リネームをRenameColumnに直す対応です。ドキュメントにあるとおり、生成されたDropColumnとAddColumnの組を、次のように置き換えます。

migrationBuilder.RenameColumn(
    name: "Name",
    table: "Customers",
    newName: "FullName");

列を差し替えるが値は残したい場合は、移行元のデータを移行先に入れ終えるまで消さない順序にします。値を計算して入れ直すときはmigrationBuilder.Sql()を使います。固定値の投入にはInsertData・UpdateData・DeleteDataが使え、これらはスクリプトやバンドルの生成でもプロバイダー向けのSQLになります。

また、Downは元の値を再現できるときだけ実装します。再現できない変換は、Downで明示的に失敗させ、ロールバックはバックアップからの復元で行う前提にします。

直したらスクリプトを再生成して、同じレビューをもう一度回します。ここまでが1周です。

この方法で見えないもの

SQLの文面から判断できることには限りがあります。次の3点は、別の手段で補います。

  • 行数と実行時間: CREATE INDEXが数秒か数十分かは、表の大きさで決まる。ステージングに本番相当のデータを置いて実測する
  • データ移行の正しさ: migrationBuilder.Sql()で書いたUPDATEが意図どおりかは、SQLを読むだけでは保証できない。コピーしたDBで流して結果を比べる
  • トランザクションの扱い: EF Coreは通常、各マイグレーションを個別のトランザクションで包む。EF Core 9ではすべての保留中マイグレーションを1つのトランザクションにまとめ、EF Core 10で元に戻っている。--no-transactionsを付けるとスクリプトからトランザクション文が除かれる

3つ目は、レビューの前提にも響きます。トランザクション文を除いたスクリプトを別の手段で流すときは、途中で失敗した場合に中途半端な状態が残りうるためです。

ロックの強さまで踏み込みたいときは、データベース側の知識が要ります。PostgreSQLを使う場合の見方はPostgresのマイグレーションをロックレベルで確かめる記事に、同じ発想のDjango版はDjangoのsqlmigrateを読む記事、Rails版はstrong_migrationsの記事にあります。EF CoreではSQL Serverを使うことが多いので、上の判定ルールはプロバイダーを書き換えて使います。

まとめ

ClaudeにC#のマイグレーションを読ませるより、dotnet ef migrations scriptのSQLを渡すほうが、見たい変更が文の形で現れます。判定ルールを表で決め、--json-schemaで結果を構造化し、highがあればCIを落とす。ここまでを自動化しても、行数や実行時間、データ移行の正しさは人とステージングの仕事です。その分担を決めておけば、DropColumnが紛れ込んでも、本番に届く前に止められます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. learn.microsoft.com/en-us/ef/core/managing-schemas/migrations
  3. learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying
  4. learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing
  5. learn.microsoft.com/en-us/ef/core/cli/dotnet
この記事を共有:XはてブLinkedIn