Start Debugging

EF Core 11 で HasData シーディングから UseAsyncSeeding へ移行する

EF Core 11 でシードデータを HasData から UseSeeding と UseAsyncSeeding へ移す手順ガイド。見落とすと既存の行を削除してしまう DeleteData マイグレーションの落とし穴も含みます。

EF Core 11 でシードデータを HasData から UseSeeding/UseAsyncSeeding へ移すのは、典型的なアプリケーションなら半日の作業です。そしてあなたを噛むのは新しい API ではありません。問題は、HasData の呼び出しを削除すると、次の dotnet ef migrations addDeleteData の命令を生成し、マイグレーションが実行されるすべてのデータベースからまさにその行を削除してしまうことです。順序を考えずに同じデプロイで HasData を削除して UseSeeding を追加すると、本番の参照データを消してしまう可能性があります。このガイドは、それを避ける順序でマイグレーションを進めます。使用するのは .NET 11、EF Core 11 (Microsoft.EntityFrameworkCore 11.0)、そして C# 14 です。中規模アプリなら午後いっぱいを見込んでください。その大半は、何年も前に移すべきだったシードを監査する時間に費やされます。

やる価値はあるでしょうか。動的なもの、キーが生成されるもの、計算されるもの、条件付きのものなら、答えはイエスで、しかも遅すぎたくらいです。本当に静的な 3 行の参照テーブルなら HasData のままにしておきましょう。ここでの目的は「すべての HasData を削除する」ことではなく、「そもそもモデルに属していなかったシードを移す」ことです。

そもそもなぜ HasData を離れるのか

HasData はシードデータをモデルのスナップショットに埋め込みます。この 1 つの設計上の事実が、離れるべきあらゆる理由の根源です。

EF チームは HasData を “model-managed data” に改名しましたが、それはまさに汎用シーディングツールとしての使用を思いとどまらせるためです。EF Core 9 で導入され EF Core 11 でも現役の UseSeedingUseAsyncSeeding は、生きた DbContext に対して実行される普通のアプリケーションコードで、これらの制限はどれもありません。どのシードを移すかまだ決めかねているなら、HasData と UseSeeding の判断マトリクスが行ごとに線引きをしてくれます。

何が壊れるか

領域変更深刻度
既存のデータベースHasData の呼び出しを削除すると DeleteData が生成され、マイグレーション時にシード行を削除する
べき等性HasData は自動的に差分が取られていた。UseSeeding は毎回実行され、手書きの存在チェックが必要
2 つのコードパスツールは同期版の UseSeeding を呼び、async 起動は UseAsyncSeeding を呼ぶ。片方だけ実装すると、もう片方は静かに何もしない
バージョン管理シードの値はマイグレーションのスナップショットを離れ、起動コードに存在するようになる。マイグレーション履歴には捕捉されなくなる
参照整合性他テーブルの外部キーが依存するデータが、マイグレーションのトランザクション内ではなく起動コールバックに置かれるようになる
テストのセットアップHasDataEnsureCreated 経由で実行されることに依存していたテストは、両方のオーバーロードを実装した場合に限り引き続き動作する

一番上の行が、インシデントレポートに載る行です。それ以外は機械的な作業です。

事前チェックリスト

シーディングのコードに 1 行でも触れる前に:

マイグレーションの手順

順序が重要です。これを間違った順序で行うことが、まさに本番の参照データを削除する方法そのものです。

1. まず UseSeeding と UseAsyncSeeding のコールバックを書く

何かを削除する前に、新しいシーディングのパスを追加します。両方のオーバーロード、同一のロジック、それぞれの先頭に存在チェックを置きます。同期版と async 版がずれないよう、本体を共有メソッドに切り出します。

// Program.cs -- .NET 11, ASP.NET Core 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(builder.Configuration.GetConnectionString("Default"))
        .UseSeeding((context, _) => SeedStatuses(context))
        .UseAsyncSeeding((context, _, ct) => SeedStatusesAsync(context, ct)));

static void SeedStatuses(DbContext context)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToHashSet();

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        context.SaveChanges();
    }
}

static async Task SeedStatusesAsync(DbContext context, CancellationToken ct)
{
    string[] required = ["Pending", "Shipped", "Delivered"];
    var existing = await context.Set<OrderStatus>()
        .Where(s => required.Contains(s.Name))
        .Select(s => s.Name)
        .ToListAsync(ct);

    var missing = required
        .Where(name => !existing.Contains(name))
        .Select(name => new OrderStatus { Name = name })
        .ToList();

    if (missing.Count > 0)
    {
        context.Set<OrderStatus>().AddRange(missing);
        await context.SaveChangesAsync(ct);
    }
}

このバージョンはもう Id を手書きで固定していない点に注目してください。データベースが割り当てます。それが要点です。もし以前 HasData でキーを手書きで割り当てていたなら、既存の外部キー参照がまさにその値を指している可能性があり、それが手順 2 を繊細にします。

検証: HasData の呼び出しをまだ残したまま、空のローカルデータベースに対してアプリケーションを実行します。重複行なしできれいに起動するはずです。存在チェックにより、2 回目の起動は何もしない(読み取りクエリ 1 回、書き込み 0 回)はずです。

2. HasData を削除する前に、既存の行をどう保存するかを決める

これは誰もが飛ばして後悔する手順です。HasData の呼び出しを削除して dotnet ef migrations add を実行すると、EF は以前シードされた各行について Up メソッドに DeleteData を生成します。

// .NET 11, EF Core 11 -- what removing HasData generates
migrationBuilder.DeleteData(
    table: "OrderStatuses",
    keyColumn: "Id",
    keyValues: new object[] { 1, 2, 3 });

既存のデータベースでは、その DELETE が実行されます。他のテーブルにこれらの行を指す外部キーがあると、削除は FOREIGN KEY constraint failed エラーで失敗するか、さらに悪いことにカスケードします。安全な選択肢は 3 つあります。

入ってくる外部キーを持つものについては、選択肢 B がほぼ常にあなたの求めるものです。行を移す必要はなく、移すのはそれらの所有権だけです。

検証: 削除マイグレーションを生成したら、適用する前に生成されたファイルを読んでください。DeleteData の呼び出しが期待どおりの行だけを対象にしていること、そして各テーブルについて A、B、C のいずれかを選んで適用したことを確認します。

3. OnModelCreating から HasData の呼び出しを削除する

さて、移すシードの HasData ブロックを削除します。

// Before -- .NET 11, EF Core 11
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderStatus>().HasData(
        new OrderStatus { Id = 1, Name = "Pending" },
        new OrderStatus { Id = 2, Name = "Shipped" },
        new OrderStatus { Id = 3, Name = "Delivered" });
}

// After -- the HasData block is gone; seeding lives in UseSeeding now
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // OrderStatus seeding moved to UseSeeding in Program.cs
}

検証: プロジェクトは引き続きコンパイルでき、dotnet ef migrations has-pending-model-changes は、これから捕捉しようとしている保留中の変更(削除されたシード)を報告します。

4. 削除マイグレーションを生成してレビューする

# .NET 11, EF Core 11
dotnet ef migrations add RemoveOrderStatusSeed

生成されたマイグレーションを開き、手順 2 での判断を適用します。選択肢 B を選んだ場合は、Up から DeleteData を、Down から対応する InsertData を削除します。モデルスナップショットの変更はそのまま残してください。それらは正しく、必要なものです。

検証: staging データベースに対して dotnet ef migrations script を実行し、SQL を読みます。行を保存するつもりだったなら、予期しない DELETE FROM OrderStatuses が生き残っていないことを確認してください。

5. staging に対して適用し、データが生き残ることを確認する

# .NET 11, EF Core 11
dotnet ef database update

その後、UseAsyncSeeding が実行されるようにアプリケーションを起動します。

検証: テーブルを照会します。参照行はちょうど 1 回だけ存在します。アプリケーションをもう一度起動し、重複が現れないことを確認します。これは存在チェックが機能していることの証明です。外部キーが行を参照している場合は、それらの関係が壊れていないことを確認してください。

マイグレーション後のスモークテスト

出荷する前に、staging に対してこのチェックリストを実行してください。

ロールバック計画

このマイグレーションは可逆ですが、細かい文字も読んでください。選択肢 B(手で DeleteData を取り除いた)を選んだ場合、スキーマレベルの Down はデータに対して何もしないため、マイグレーションをロールバックしても行はそのまま残り、モデルスナップショットが復元されるだけです。HasData ブロックを再追加して新しいマイグレーションを生成すれば旧世界に戻りますが、EF は不足している行を InsertData しようとします。

選択肢 A(削除と再シードを起こさせた)を選んだ場合、ロールバックはより危険です。行は今やデータベース生成のキーを持つため、固定キーを期待する HasData モデルへ戻すと不一致になります。その場合、ライブデータのインプレースロールバックを試みてはいけません。事前チェックの手順で取ったバックアップから復元してください。だからこそ事前バックアップは任意ではないのです。

私たちがはまった落とし穴

エンティティが record であっても、これらは何も変わりません: record は EF Core 11 で正しく動作しますHasData でも UseSeeding でも同じです。そしてついでにデータ層を引き締めているなら、同じ「何がいつ実行されるか」の規律が 監査のための EF Core 11 インターセプターにも現れます。

関連記事

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る