データのシード処理

データ シーディングは、データベースに初期データ セットを設定するプロセスです。

EF Core では、次のいくつかの方法で実現できます。

構成オプション UseSeedingUseAsyncSeeding 方法

EF 9 では、初期データをデータベースにシード処理する便利な方法を提供するメソッド UseSeedingUseAsyncSeeding が導入されました。 これらのメソッドは、カスタム初期化ロジックを使用するエクスペリエンスを向上することを目的としています (以下で説明します)。 すべてのデータ シード 処理コードを配置できる 1 つの明確な場所が提供されます。 さらに、 UseSeeding および UseAsyncSeeding メソッド内のコードは、同時実行の問題を防ぐために 移行ロック メカニズム によって保護されています。

モデルの変更や移行が適用されていない場合でも、新しいシード方法は EnsureCreated 操作、 Migrate および dotnet ef database update コマンドの一部として呼び出されます。

ヒント

EF Core を操作するときにデータベースに初期データをシードするには、 UseSeedingUseAsyncSeeding を使用することをお勧めします。

これらのメソッドは、 オプションの構成手順で設定できます。 例を次に示します。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

UseSeeding / UseAsyncSeeding は、 EnsureCreated/EnsureCreatedAsync 中と移行の適用後に呼び出されます (たとえば、 Migrate/MigrateAsyncdotnet ef database update、移行バンドル)。 EF Core のツールとバンドルは現在、同期デリゲートに依存しているため、アプリケーションが通常非同期 API を使用する場合でも、常に UseSeeding を実装します。

デプロイ動作

UseSeedingUseAsyncSeeding は、EF Core がデータベースの初期化または移行操作を実行する場合にのみ実行されます。 それに応じてデプロイ メカニズムを選択します。

Operation 呼び出されたデリゲートのシード処理
EnsureCreated または Migrate UseSeeding
EnsureCreatedAsync または MigrateAsync UseAsyncSeeding
dotnet ef database update または Update-Database UseSeeding
移行バンドル UseSeeding
外部 SQL ツールによって実行される SQL スクリプト None

UseSeeding実行する必要がある自動デプロイの場合は、移行バンドルまたは専用の初期化プロセスを使用します。 EF Core のツールとバンドルは同期デリゲートを呼び出すので、アプリケーションが通常非同期 API を使用する場合でも、常に UseSeeding を実装します。 レビューまたは DBA の実行が必要で、代わりにシード データが移行操作によって表される場合は、SQL スクリプトを使用します。 トレードオフについては、「 移行の適用 」を参照してください。

シード処理は、移行のダウングレード後にも実行されます。 シード処理コードで使用されるすべてのテーブルが含まれていない移行へのダウングレードがアプリケーションでサポートされている場合は、クエリを実行する前に、必要なスキーマが存在することを確認してください。 これは、 0をターゲットにして、すべての移行を元に戻す場合に特に重要です。

熱望アプリケーションは、ローカル移行の実行を調整し、移行バンドルまたはスクリプトを 、移行の熱望 EF Core 移行統合と共に発行できます。

カスタム初期化ロジック

データのシード処理を実行するための簡単で強力な方法は、メインのアプリケーション ロジックの実行が開始される前に SaveChangesAsync を使用することです。 その目的のために UseSeedingUseAsyncSeeding を使用することをお勧めしますが、これらの方法を使用することは良い解決策ではない場合があります。 シナリオの例として、シード処理で 1 つのトランザクションで 2 つの異なるコンテキストを使用する必要がある場合があります。 アプリケーションでカスタム初期化を直接実行するコード サンプルを次に示します。

using (var context = new DataSeedingContext())
{
    await context.Database.EnsureCreatedAsync();

    var testBlog = await context.Blogs.FirstOrDefaultAsync(b => b.Url == "http://test.com");
    if (testBlog == null)
    {
        context.Blogs.Add(new Blog { Url = "http://test.com" });
        await context.SaveChangesAsync();
    }
}

警告

シード処理コードは、複数のインスタンスが実行されるときにコンカレンシーの問題の原因となる可能性があり、データベース スキーマを変更するアクセス許可をアプリに持たせることも必要になるため、通常のアプリの実行には含めないでください。

展開の制約に応じて、初期化コードはさまざまな方法で実行できます。

  • 初期化アプリをローカルで実行する
  • メイン アプリと一緒に初期化アプリをデプロイし、初期化ルーチンを呼び出し、初期化アプリを無効化または削除する

これは通常、発行プロファイルを使用して自動化できます。

モデル管理データ

データは、モデル構成の一部としてエンティティ型に関連付けることもできます。 EF Core の 移行 では、データベースをモデルの新しいバージョンにアップグレードするときに、挿入、更新、または削除のどの操作を適用する必要があるかを自動的に計算できます。

警告

移行では、管理対象データを目的の状態にするために実行する必要がある操作を決定する際に、モデルの変更のみが考慮されます。 そのため、移行の外部で実行されたデータの変更が失われたり、エラーの原因になったりする可能性があります。

たとえば、これは CountryOnModelCreating に対して管理対象データを構成します。

modelBuilder.Entity<Country>(b =>
{
    b.Property(x => x.Name).IsRequired();
    b.HasData(
        new Country { CountryId = 1, Name = "USA" },
        new Country { CountryId = 2, Name = "Canada" },
        new Country { CountryId = 3, Name = "Mexico" });
});

リレーションシップを持つエンティティを追加するには、外部キー値を指定する必要があります。

modelBuilder.Entity<City>().HasData(
    new City { Id = 1, Name = "Seattle", LocatedInId = 1 },
    new City { Id = 2, Name = "Vancouver", LocatedInId = 2 },
    new City { Id = 3, Name = "Mexico City", LocatedInId = 3 },
    new City { Id = 4, Name = "Puebla", LocatedInId = 3 });

多対多ナビゲーションのデータを管理する場合は、結合エンティティを明示的に構成する必要があります。 エンティティタイプにシャドウ状態のプロパティがある場合 (たとえば、以下の LanguageCountry 結合エンティティ)、匿名クラスを使用して値を提供できます。

modelBuilder.Entity<Language>(b =>
{
    b.HasData(
        new Language { Id = 1, Name = "English" },
        new Language { Id = 2, Name = "French" },
        new Language { Id = 3, Name = "Spanish" });

    b.HasMany(x => x.UsedIn)
        .WithMany(x => x.OfficialLanguages)
        .UsingEntity(
            "LanguageCountry",
            r => r.HasOne(typeof(Country)).WithMany().HasForeignKey("CountryId").HasPrincipalKey(nameof(Country.CountryId)),
            l => l.HasOne(typeof(Language)).WithMany().HasForeignKey("LanguageId").HasPrincipalKey(nameof(Language.Id)),
            je =>
            {
                je.HasKey("LanguageId", "CountryId");
                je.HasData(
                    new { LanguageId = 1, CountryId = 2 },
                    new { LanguageId = 2, CountryId = 2 },
                    new { LanguageId = 3, CountryId = 3 });
            });
});

所有エンティティ型は、同様の方法で構成できます。

modelBuilder.Entity<Language>().OwnsOne(p => p.Details).HasData(
    new { LanguageId = 1, Phonetic = false, Tonal = false, PhonemesCount = 44 },
    new { LanguageId = 2, Phonetic = false, Tonal = false, PhonemesCount = 36 },
    new { LanguageId = 3, Phonetic = true, Tonal = false, PhonemesCount = 24 });

詳細なコンテキストについては、完全なサンプル プロジェクトに関するページを参照してください。

データがモデルに追加されたら、移行を使用して変更を適用する必要があります。

HasData 変更は、移行がスキャフォールディングされるときに、 InsertDataUpdateData、および DeleteData 操作に変換されます。 Migrateを呼び出しても、現在のHasData構成は個別に検査されません。 モデル管理データを変更した後、新しい移行を追加してデプロイします。

ヒント

自動デプロイの場合は、 移行バンドルを使用します。 SQL スクリプトは、実行前にプレビューまたは変更する必要がある場合に使用します。

または、 EnsureCreatedAsync を使用して、マネージ データを含む新しいデータベースを作成することもできます (たとえば、テスト データベースの場合や、インメモリ プロバイダーや非リレーショナル データベースを使用する場合など)。 データベースがすでに存在する場合は、 EnsureCreatedAsync はスキーマもデータベース内の管理データも更新しないことに注意してください。 リレーショナル データベースの場合、移行を使用する予定がある場合は、EnsureCreatedAsync を呼び出さないでください。

"データ シード処理" と呼ばれた HasData メソッドを使用してデータベースにデータを設定します。 この名前付けでは、この機能にはいくつかの制限があり、特定の種類のデータにのみ適しており、不適切な期待が設定されます。 そのため、名前を "モデルマネージド データ" に変更することにしました。 UseSeeding および UseAsyncSeeding メソッドは、汎用データ シード処理に使用する必要があります。

モデル管理データの制限事項

この種類のデータは移行によって管理され、データベースに既に存在するデータを更新するスクリプトは、データベースに接続せずに生成する必要があります。 これによって、いくつかの制限が課されます。

  • 主キーの値は、通常はデータベースによって生成される場合でも、指定する必要があります。 移行間のデータ変更を検出するために使用されます。
  • 以前に挿入されたデータは、主キーが何らかの方法で変更された場合、削除されます。

したがって、この機能は移行の外部で変更されることが想定されておらず、データベース内の他のものに依存しない、郵便番号などの静的データに対して最も役立ちます。

シナリオに次のいずれかが含まれている場合は、最初のセクションで説明されている UseSeedingUseAsyncSeeding メソッドを使用することをお勧めします。

  • テスト用の一時データ
  • データベースの状態に依存するデータ
  • サイズの大きいデータ (シード用データは移行スナップショットでキャプチャされ、大きなデータはすぐに非常に大きなファイルになり、パフォーマンスが低下する可能性があります)。
  • データベースによって生成されるキー値を必要とするデータ (代替キーを ID として使用するエンティティなど)
  • 一部のパスワード ハッシュなど、(値の変換によって処理されない) カスタム変換を必要とするデータ
  • ASP.NET Core ID ロールやユーザーの作成など、外部 API の呼び出しを必要とするデータ
  • 固定されていない確定的なデータ ( DateTime.Nowへのシード処理など)。

手動による移行のカスタマイズ

移行が追加されると、HasData で指定したデータに加えた変更が、InsertData()UpdateData()、および DeleteData() の呼び出しに変換されます。 HasData の制限事項を回避する方法の 1 つとして、これらの呼び出しまたはカスタム操作を移行に手動で追加することがあります。

migrationBuilder.InsertData(
    table: "Countries",
    columns: new[] { "CountryId", "Name" },
    values: new object[,]
    {
        { 1, "USA" },
        { 2, "Canada" },
        { 3, "Mexico" }
    });

migrationBuilder.InsertData(
    table: "Languages",
    columns: new[] { "Id", "Name", "Details_PhonemesCount", "Details_Phonetic", "Details_Tonal" },
    values: new object[,]
    {
        { 1, "English", 44, false, false },
        { 2, "French", 36, false, false },
        { 3, "Spanish", 24, true, false }
    });

migrationBuilder.InsertData(
    table: "Cites",
    columns: new[] { "Id", "LocatedInId", "Name" },
    values: new object[,]
    {
        { 1, 1, "Seattle" },
        { 2, 2, "Vancouver" },
        { 3, 3, "Mexico City" },
        { 4, 3, "Puebla" }
    });

migrationBuilder.InsertData(
    table: "LanguageCountry",
    columns: new[] { "CountryId", "LanguageId" },
    values: new object[,]
    {
        { 2, 1 },
        { 2, 2 },
        { 3, 3 }
    });

これらの操作は、移行の書き込み時に値とキーが固定されている場合に適しています。 現在のデータベースの状態に対するクエリは実行されません。 InsertDataUpdateDataDeleteData、プロバイダー固有の SQL 変換の例については、移行でのデータ操作に関するページを参照してください。