Data Seeding
Penyemaian data adalah proses mengisi database dengan sekumpulan data awal.
Ada beberapa cara yang dapat dicapai dalam EF Core:
-
Penyemaian data opsi konfigurasi (
UseSeeding
) - Logika inisialisasi kustom
-
Data terkelola model (
HasData
) - Kustomisasi migrasi manual
Opsi UseSeeding
dan UseAsyncSeeding
metode konfigurasi
EF 9 diperkenalkan UseSeeding
dan UseAsyncSeeding
metode, yang menyediakan cara mudah untuk menyemai database dengan data awal. Metode ini bertujuan untuk meningkatkan pengalaman menggunakan logika inisialisasi kustom (dijelaskan di bawah). Mereka menyediakan satu lokasi yang jelas di mana semua kode penyemaian data dapat ditempatkan. Selain itu, kode di dalam UseSeeding
dan UseAsyncSeeding
metode dilindungi oleh mekanisme penguncian migrasi untuk mencegah masalah konkurensi.
Metode penyemaian baru disebut sebagai bagian EnsureCreated
dari operasi, Migrate
dan dotnet ef database update
perintah, bahkan jika tidak ada perubahan model dan tidak ada migrasi yang diterapkan.
Tip
Menggunakan UseSeeding
dan UseAsyncSeeding
merupakan cara yang direkomendasikan untuk menyemai database dengan data awal saat bekerja dengan EF Core.
Metode ini dapat disiapkan dalam langkah konfigurasi opsi. Berikut adalah contoh:
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);
}
});
Catatan
UseSeeding
dipanggil dari EnsureCreated
metode , dan UseAsyncSeeding
dipanggil dari EnsureCreatedAsync
metode . Saat menggunakan fitur ini, disarankan untuk mengimplementasikan metode UseSeeding
dan UseAsyncSeeding
menggunakan logika serupa, bahkan jika kode yang menggunakan EF tidak sinkron. Alat EF Core saat ini bergantung pada versi sinkron metode dan tidak akan menyemai database dengan benar jika UseSeeding
metode tidak diimplementasikan.
Logika inisialisasi kustom
Cara mudah dan kuat untuk melakukan penyemaian data adalah dengan menggunakan DbContext.SaveChangesAsync()
sebelum logika aplikasi utama memulai eksekusi. Disarankan untuk menggunakan UseSeeding
dan UseAsyncSeeding
untuk tujuan itu, namun terkadang menggunakan metode ini bukanlah solusi yang baik. Contoh skenarionya adalah ketika penyemaian memerlukan penggunaan dua konteks yang berbeda dalam satu transaksi. Di bawah ini adalah sampel kode yang melakukan inisialisasi kustom dalam aplikasi secara langsung:
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();
}
}
Peringatan
Kode penyemaian tidak boleh menjadi bagian dari eksekusi aplikasi normal karena ini dapat menyebabkan masalah konkurensi saat beberapa instans berjalan dan juga akan mengharuskan aplikasi memiliki izin untuk mengubah skema database.
Bergantung pada batasan penyebaran Anda, kode inisialisasi dapat dijalankan dengan cara yang berbeda:
- Menjalankan aplikasi inisialisasi secara lokal
- Menyebarkan aplikasi inisialisasi dengan aplikasi utama, memanggil rutinitas inisialisasi dan menonaktifkan atau menghapus aplikasi inisialisasi.
Ini biasanya dapat diotomatisasi dengan menggunakan profil publikasi.
Data terkelola model
Data juga dapat dikaitkan dengan jenis entitas sebagai bagian dari konfigurasi model. Kemudian, migrasi EF Core dapat secara otomatis menghitung operasi penyisipan, pembaruan, atau penghapusan apa yang perlu diterapkan saat meningkatkan database ke versi model baru.
Peringatan
Migrasi hanya mempertimbangkan perubahan model saat menentukan operasi apa yang harus dilakukan untuk memasukkan data terkelola ke dalam status yang diinginkan. Dengan demikian setiap perubahan pada data yang dilakukan di luar migrasi mungkin hilang atau menyebabkan kesalahan.
Sebagai contoh, ini akan mengonfigurasi data terkelola Country
untuk di OnModelCreating
:
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" });
});
Untuk menambahkan entitas yang memiliki hubungan, nilai kunci asing perlu ditentukan:
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 });
Saat mengelola data untuk navigasi banyak ke banyak, entitas gabungan perlu dikonfigurasi secara eksplisit. Jika jenis entitas memiliki properti apa pun dalam status bayangan (misalnya LanguageCountry
entitas gabungan di bawah), kelas anonim dapat digunakan untuk memberikan nilai:
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 });
});
});
Jenis entitas yang dimiliki dapat dikonfigurasi dengan cara yang sama:
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 });
Lihat proyek sampel lengkap untuk konteks selengkapnya.
Setelah data ditambahkan ke model, migrasi harus digunakan untuk menerapkan perubahan.
Tip
Jika Anda perlu menerapkan migrasi sebagai bagian dari penyebaran otomatis, Anda dapat membuat skrip SQL yang dapat dipratinjau sebelum eksekusi.
Atau, Anda dapat menggunakan context.Database.EnsureCreatedAsync()
untuk membuat database baru yang berisi data terkelola, misalnya untuk database pengujian atau saat menggunakan penyedia dalam memori atau database non-relasional apa pun. Perhatikan bahwa jika database sudah ada, EnsureCreatedAsync()
tidak akan memperbarui skema atau data terkelola dalam database. Untuk database relasional, Anda tidak boleh memanggil EnsureCreatedAsync()
jika Anda berencana menggunakan Migrasi.
Catatan
Mengisi database menggunakan metode yang HasData
digunakan untuk disebut sebagai "seeding data". Penamaan ini menetapkan ekspektasi yang salah, karena fitur ini memiliki sejumlah batasan dan hanya sesuai untuk jenis data tertentu. Itulah sebabnya kami memutuskan untuk mengganti namanya menjadi "data terkelola model".
UseSeeding
metode dan UseAsyncSeeding
harus digunakan untuk penyemaian data tujuan umum.
Batasan data terkelola model
Jenis data ini dikelola oleh migrasi dan skrip untuk memperbarui data yang sudah ada dalam database perlu dibuat tanpa menyambungkan ke database. Ini memberlakukan beberapa batasan:
- Nilai kunci utama perlu ditentukan meskipun biasanya dihasilkan oleh database. Ini akan digunakan untuk mendeteksi perubahan data antara migrasi.
- Data yang disisipkan sebelumnya akan dihapus jika kunci utama diubah dengan cara apa pun.
Oleh karena itu fitur ini paling berguna untuk data statis yang tidak diharapkan berubah di luar migrasi dan tidak bergantung pada hal lain dalam database, misalnya kode pos.
Jika skenario Anda menyertakan salah satu hal berikut, disarankan untuk menggunakan UseSeeding
metode dan UseAsyncSeeding
yang dijelaskan di bagian pertama:
- Data sementara untuk pengujian
- Data yang bergantung pada status database
- Data yang besar (data penyemaian diambil dalam rekam jepret migrasi, dan data besar dapat dengan cepat menyebabkan file besar dan performa yang terdegradasi).
- Data yang membutuhkan nilai kunci yang akan dihasilkan oleh database, termasuk entitas yang menggunakan kunci alternatif sebagai identitas
- Data yang memerlukan transformasi kustom (yang tidak ditangani oleh konversi nilai), seperti beberapa hash kata sandi
- Data yang memerlukan panggilan ke API eksternal, seperti peran identitas inti ASP.NET dan pembuatan pengguna
- Data yang tidak tetap dan deterministik, seperti penyemaian ke
DateTime.Now
.
Kustomisasi migrasi manual
Saat migrasi ditambahkan, perubahan pada data yang ditentukan dengan HasData
diubah menjadi panggilan ke InsertData()
, UpdateData()
, dan DeleteData()
. Salah satu cara mengatasi beberapa batasan HasData
adalah dengan menambahkan panggilan atau operasi kustom ini secara manual ke migrasi sebagai gantinya.
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 }
});