データのシード処理
データ シーディングは、データベースに初期データ セットを設定するプロセスです。
EF Core では、次のいくつかの方法で実現できます。
構成オプション UseSeeding
と UseAsyncSeeding
方法
EF 9 では、初期データをデータベースにシード処理する便利な方法を提供するメソッド UseSeeding
と UseAsyncSeeding
が導入されました。 これらのメソッドは、カスタム初期化ロジックを使用するエクスペリエンスを向上することを目的としています (以下で説明します)。 すべてのデータ シード 処理コードを配置できる 1 つの明確な場所が提供されます。 さらに、 UseSeeding
および UseAsyncSeeding
メソッド内のコードは、同時実行の問題を防ぐために 移行ロック メカニズム によって保護されています。
モデルの変更や移行が適用されていない場合でも、新しいシード方法は EnsureCreated
操作、 Migrate
および dotnet ef database update
コマンドの一部として呼び出されます。
ヒント
EF Core を操作するときにデータベースに初期データをシードするには、 UseSeeding
と UseAsyncSeeding
を使用することをお勧めします。
これらのメソッドは、 オプションの構成手順で設定できます。 例を次に示します。
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);
}
});
Note
UseSeeding
は EnsureCreated
メソッドから呼び出され、 UseAsyncSeeding
は EnsureCreatedAsync
メソッドから呼び出されます。 この機能を使用する場合は、EF を使用するコードが非同期であっても、同様のロジックを使用して両方 UseSeeding
と UseAsyncSeeding
メソッドを実装することをお勧めします。 EF Core ツールは現在、メソッドの同期バージョンに依存しており、 UseSeeding
メソッドが実装されていない場合、データベースを正しくシード処理しません。
カスタム初期化ロジック
データのシード処理を実行するための簡単で強力な方法は、メインのアプリケーション ロジックの実行が開始される前に DbContext.SaveChanges()
を使用することです。 その目的のために UseSeeding
と UseAsyncSeeding
を使用することをお勧めしますが、これらの方法を使用することは良い解決策ではない場合があります。 シナリオの例として、シード処理で 1 つのトランザクションで 2 つの異なるコンテキストを使用する必要がある場合があります。 アプリケーションでカスタム初期化を直接実行するコード サンプルを次に示します。
using (var context = new DataSeedingContext())
{
context.Database.EnsureCreated();
var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Blogs.Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
}
警告
シード処理コードは、複数のインスタンスが実行されるときにコンカレンシーの問題の原因となる可能性があり、データベース スキーマを変更するアクセス許可をアプリに持たせることも必要になるため、通常のアプリの実行には含めないでください。
展開の制約に応じて、初期化コードはさまざまな方法で実行できます。
- 初期化アプリをローカルで実行する
- メイン アプリと一緒に初期化アプリをデプロイし、初期化ルーチンを呼び出し、初期化アプリを無効化または削除する
これは通常、発行プロファイルを使用して自動化できます。
モデル管理データ
データは、モデル構成の一部としてエンティティ型に関連付けることもできます。 EF Core の 移行 では、データベースをモデルの新しいバージョンにアップグレードするときに、挿入、更新、または削除のどの操作を適用する必要があるかを自動的に計算できます。
警告
移行では、管理対象データを目的の状態にするために実行する必要がある操作を決定する際に、モデルの変更のみが考慮されます。 そのため、移行の外部で実行されたデータの変更が失われたり、エラーの原因になったりする可能性があります。
たとえば、これは OnModelCreating
の Country
に対して管理対象データを構成します。
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 });
詳細なコンテキストについては、完全なサンプル プロジェクトに関するページを参照してください。
データがモデルに追加されたら、移行を使用して変更を適用する必要があります。
ヒント
自動展開の一部として移行を適用する必要がある場合は、実行前にプレビューできる SQL スクリプトを作成できます。
または、 context.Database.EnsureCreated()
を使用して、マネージ データを含む新しいデータベースを作成することもできます (たとえば、テスト データベースの場合や、インメモリ プロバイダーや非リレーショナル データベースを使用する場合など)。 データベースがすでに存在する場合は、 EnsureCreated()
はスキーマもデータベース内の管理データも更新しないことに注意してください。 リレーショナル データベースの場合、移行を使用する予定がある場合は、EnsureCreated()
を呼び出さないでください。
Note
"データ シード処理" と呼ばれた HasData
メソッドを使用してデータベースにデータを設定します。 この名前付けでは、この機能にはいくつかの制限があり、特定の種類のデータにのみ適しており、不適切な期待が設定されます。 そのため、名前を "モデルマネージド データ" に変更することにしました。 UseSeeding
および UseAsyncSeeding
メソッドは、汎用データ シード処理に使用する必要があります。
モデル管理データの制限事項
この種類のデータは移行によって管理され、データベースに既に存在するデータを更新するスクリプトは、データベースに接続せずに生成する必要があります。 これによって、いくつかの制限が課されます。
- 主キーの値は、通常はデータベースによって生成される場合でも、指定する必要があります。 移行間のデータ変更を検出するために使用されます。
- 以前に挿入されたデータは、主キーが何らかの方法で変更された場合、削除されます。
したがって、この機能は移行の外部で変更されることが想定されておらず、データベース内の他のものに依存しない、郵便番号などの静的データに対して最も役立ちます。
シナリオに次のいずれかが含まれている場合は、最初のセクションで説明されている UseSeeding
と UseAsyncSeeding
メソッドを使用することをお勧めします。
- テスト用の一時データ
- データベースの状態に依存するデータ
- サイズの大きいデータ (シード用データは移行スナップショットでキャプチャされ、大きなデータはすぐに非常に大きなファイルになり、パフォーマンスが低下する可能性があります)。
- データベースによって生成されるキー値を必要とするデータ (代替キーを ID として使用するエンティティなど)
- 一部のパスワード ハッシュなど、(値の変換によって処理されない) カスタム変換を必要とするデータ
- 外部 API の呼び出しを必要とするデータ (ASP.NET Core ID ロールやユーザーの作成など)
手動による移行のカスタマイズ
移行が追加されると、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 }
});
.NET