Yang Baru di EF Core 8
EF Core 8.0 (EF8) dirilis pada November 2023.
Tip
Anda dapat menjalankan dan men-debug ke dalam sampel dengan mengunduh kode sampel dari GitHub. Setiap bagian menautkan ke kode sumber khusus untuk bagian tersebut.
EF8 memerlukan .NET 8 SDK untuk membangun dan memerlukan runtime .NET 8 untuk dijalankan. EF8 tidak akan berjalan pada versi .NET sebelumnya, dan tidak akan berjalan pada .NET Framework.
Objek nilai menggunakan Jenis Kompleks
Objek yang disimpan ke database dapat dibagi menjadi tiga kategori luas:
- Objek yang tidak terstruktur dan menyimpan satu nilai. Misalnya,
int
,Guid
,string
,IPAddress
. Ini (agak longgar) yang disebut "jenis primitif". - Objek yang disusun untuk menyimpan beberapa nilai, dan di mana identitas objek ditentukan oleh nilai kunci. Misalnya,
Blog
,Post
,Customer
. Ini disebut "jenis entitas". - Objek yang disusun untuk menyimpan beberapa nilai, tetapi objek tidak memiliki kunci yang menentukan identitas. Misalnya,
Address
,Coordinate
.
Sebelum EF8, tidak ada cara yang baik untuk memetakan jenis objek ketiga. Jenis yang dimiliki dapat digunakan, tetapi karena jenis yang dimiliki sebenarnya adalah jenis entitas, mereka memiliki semantik berdasarkan nilai kunci, bahkan ketika nilai kunci tersebut disembunyikan.
EF8 sekarang mendukung "Jenis Kompleks" untuk mencakup jenis objek ketiga ini. Objek jenis kompleks:
- Tidak diidentifikasi atau dilacak oleh nilai kunci.
- Harus didefinisikan sebagai bagian dari jenis entitas. (Dengan kata lain, Anda tidak dapat memiliki
DbSet
tipe kompleks.) - Dapat berupa jenis nilai .NET atau jenis referensi.
- Instans dapat dibagikan oleh beberapa properti.
Contoh sederhana
Misalnya, pertimbangkan Address
jenis:
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Address
kemudian digunakan di tiga tempat dalam model pelanggan/pesanan sederhana:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Address Address { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Mari kita buat dan simpan pelanggan dengan alamat mereka:
var customer = new Customer
{
Name = "Willow",
Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" }
};
context.Add(customer);
await context.SaveChangesAsync();
Ini menghasilkan baris berikut yang disisipkan ke dalam database:
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Perhatikan bahwa jenis kompleks tidak mendapatkan tabel mereka sendiri. Sebagai gantinya, tabel disimpan sebaris ke kolom Customers
tabel. Ini cocok dengan perilaku berbagi tabel dari jenis yang dimiliki.
Catatan
Kami tidak berencana untuk mengizinkan jenis kompleks dipetakan ke tabel mereka sendiri. Namun, dalam rilis mendatang, kami berencana untuk mengizinkan jenis kompleks disimpan sebagai dokumen JSON dalam satu kolom. Pilih Masalah #31252 jika ini penting bagi Anda.
Sekarang katakanlah kita ingin mengirim pesanan ke pelanggan dan menggunakan alamat pelanggan sebagai penagihan default alamat pengiriman. Cara alami untuk melakukan ini adalah dengan menyalin Address
objek dari ke Customer
dalam Order
. Contohnya:
customer.Orders.Add(
new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, });
await context.SaveChangesAsync();
Dengan jenis kompleks, ini berfungsi seperti yang diharapkan, dan alamat disisipkan ke Orders
dalam tabel:
INSERT INTO [Orders] ([Contents], [CustomerId],
[BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1], [BillingAddress_Line2], [BillingAddress_PostCode],
[ShippingAddress_City], [ShippingAddress_Country], [ShippingAddress_Line1], [ShippingAddress_Line2], [ShippingAddress_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);
Sejauh ini Anda mungkin mengatakan, "tetapi saya bisa melakukan ini dengan jenis yang dimiliki!" Namun, semantik "jenis entitas" dari jenis yang dimiliki dengan cepat menghalangi. Misalnya, menjalankan kode di atas dengan jenis yang dimiliki menghasilkan banyak peringatan dan kemudian kesalahan:
warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update)
An exception occurred in the database while saving changes for context type 'NewInEfCore8.ComplexTypesSample+CustomerContext'.
System.InvalidOperationException: Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
Ini karena satu instans jenis Address
entitas (dengan nilai kunci tersembunyi yang sama) digunakan untuk tiga instans entitas yang berbeda . Di sisi lain, berbagi instans yang sama antara properti kompleks diizinkan, sehingga kode berfungsi seperti yang diharapkan saat menggunakan jenis kompleks.
Konfigurasi jenis kompleks
Jenis kompleks harus dikonfigurasi dalam model menggunakan atribut pemetaan atau dengan memanggilComplexProperty
API di OnModelCreating
. Jenis kompleks tidak ditemukan oleh konvensi.
Misalnya, Address
jenis dapat dikonfigurasi menggunakan ComplexTypeAttribute:
[ComplexType]
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Atau di OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.ComplexProperty(e => e.Address);
modelBuilder.Entity<Order>(b =>
{
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Mutabilitas
Dalam contoh di atas, kami berakhir dengan instans yang sama yang Address
digunakan di tiga tempat. Ini diizinkan dan tidak menyebabkan masalah apa pun untuk EF Core saat menggunakan jenis kompleks. Namun, berbagi instans dengan jenis referensi yang sama berarti bahwa jika nilai properti pada instans dimodifikasi, perubahan tersebut akan tercermin dalam ketiga penggunaan. Misalnya, mengikuti dari atas, mari kita ubah Line1
alamat pelanggan dan simpan perubahan:
customer.Address.Line1 = "Peacock Lodge";
await context.SaveChangesAsync();
Ini menghasilkan pembaruan berikut ke database saat menggunakan SQL Server:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] = @p3
OUTPUT 1
WHERE [Id] = @p4;
Perhatikan bahwa ketiga Line1
kolom telah berubah, karena semuanya berbagi instans yang sama. Ini biasanya tidak seperti yang kita inginkan.
Tip
Jika alamat pesanan harus berubah secara otomatis saat alamat pelanggan berubah, maka pertimbangkan untuk memetakan alamat sebagai jenis entitas. Order
dan Customer
kemudian dapat dengan aman mereferensikan instans alamat yang sama (yang sekarang diidentifikasi oleh kunci) melalui properti navigasi.
Cara yang baik untuk menangani masalah seperti ini adalah dengan membuat jenis tidak dapat diubah. Memang, kekekalan ini sering kali wajar ketika jenis adalah kandidat yang baik untuk menjadi jenis yang kompleks. Misalnya, biasanya masuk akal untuk menyediakan objek baru Address
yang kompleks daripada hanya bermutasi, katakanlah, negara sambil membiarkan sisanya sama.
Jenis referensi dan nilai dapat dibuat tidak dapat diubah. Kita akan melihat beberapa contoh di bagian berikut.
Jenis referensi sebagai jenis kompleks
Kelas yang tidak dapat diubah
Kami menggunakan yang sederhana dan dapat class
diubah dalam contoh di atas. Untuk mencegah masalah dengan mutasi yang tidak disengaja yang dijelaskan di atas, kita dapat membuat kelas tidak dapat diubah. Contohnya:
public class Address
{
public Address(string line1, string? line2, string city, string country, string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}
public string Line1 { get; }
public string? Line2 { get; }
public string City { get; }
public string Country { get; }
public string PostCode { get; }
}
Tip
Dengan C# 12 atau lebih tinggi, definisi kelas ini dapat disederhanakan menggunakan konstruktor utama:
public class Address(string line1, string? line2, string city, string country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}
Sekarang tidak dimungkinkan Line1
untuk mengubah nilai pada alamat yang ada. Sebagai gantinya, kita perlu membuat instans baru dengan nilai yang diubah. Contohnya:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Kali ini panggilan untuk SaveChangesAsync
hanya memperbarui alamat pelanggan:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Perhatikan bahwa meskipun objek Alamat tidak dapat diubah, dan seluruh objek telah diubah, EF masih melacak perubahan pada properti individual, jadi hanya kolom dengan nilai yang diubah yang diperbarui.
Rekaman yang tidak dapat diubah
C# 9 memperkenalkan jenis rekaman, yang membuat pembuatan dan penggunaan objek yang tidak dapat diubah lebih mudah. Misalnya, Address
objek dapat dibuat sebagai jenis rekaman:
public record Address
{
public Address(string line1, string? line2, string city, string country, string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}
public string Line1 { get; init; }
public string? Line2 { get; init; }
public string City { get; init; }
public string Country { get; init; }
public string PostCode { get; init; }
}
Tip
Definisi rekaman ini dapat disederhanakan menggunakan konstruktor utama:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
Mengganti objek yang dapat diubah dan memanggil SaveChanges
sekarang memerlukan lebih sedikit kode:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Jenis nilai sebagai jenis kompleks
Struktur yang dapat diubah
Jenis nilai yang dapat diubah sederhana dapat digunakan sebagai jenis kompleks. Misalnya, Address
dapat didefinisikan sebagai struct
dalam C#:
public struct Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Menetapkan objek pelanggan Address
ke properti pengiriman dan penagihan Address
menghasilkan setiap properti mendapatkan salinan Address
, karena ini adalah cara kerja jenis nilai. Ini berarti bahwa memodifikasi Address
pada pelanggan tidak akan mengubah instans pengiriman atau penagihan Address
, sehingga struktur yang dapat diubah tidak memiliki masalah berbagi instans yang sama yang terjadi dengan kelas yang dapat diubah.
Namun, struktur yang dapat diubah umumnya tidak dianjurkan dalam C#, jadi pikirkan dengan sangat hati-hati sebelum menggunakannya.
Struktur yang tidak dapat diubah
Struktur yang tidak dapat diubah berfungsi dengan baik seperti jenis kompleks, seperti halnya kelas yang tidak dapat diubah. Misalnya, Address
dapat didefinisikan sih sehingga tidak dapat dimodifikasi:
public readonly struct Address(string line1, string? line2, string city, string country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}
Kode untuk mengubah alamat sekarang terlihat sama seperti saat menggunakan kelas yang tidak dapat diubah:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Rekaman struct yang tidak dapat diubah
Jenis yang diperkenalkan struct record
C# 10, yang memudahkan untuk membuat dan bekerja dengan catatan struct yang tidak dapat diubah seperti itu dengan catatan kelas yang tidak dapat diubah. Misalnya, kita dapat mendefinisikan Address
sebagai rekaman struct yang tidak dapat diubah:
public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode);
Kode untuk mengubah alamat sekarang terlihat sama seperti saat menggunakan rekaman kelas yang tidak dapat diubah:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Jenis kompleks berlapis
Jenis kompleks dapat berisi properti dari jenis kompleks lainnya. Misalnya, mari kita gunakan jenis kompleks kita Address
dari atas bersama dengan PhoneNumber
jenis kompleks, dan sarangkan keduanya di dalam jenis kompleks lain:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
public record PhoneNumber(int CountryCode, long Number);
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
public required PhoneNumber MobilePhone { get; init; }
}
Kami menggunakan catatan yang tidak dapat diubah di sini, karena ini adalah kecocokan yang baik untuk semantik jenis kompleks kami, tetapi bersarangnya jenis kompleks dapat dilakukan dengan rasa apa pun dari jenis .NET.
Catatan
Kami tidak menggunakan konstruktor utama untuk jenis tersebut Contact
karena EF Core belum mendukung injeksi konstruktor dari nilai jenis kompleks. Pilih Masalah #31621 jika ini penting bagi Anda.
Kami akan menambahkan Contact
sebagai properti dari Customer
:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Contact Contact { get; set; }
public List<Order> Orders { get; } = new();
}
Dan PhoneNumber
sebagai properti dari Order
:
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required PhoneNumber ContactPhone { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Konfigurasi jenis kompleks berlapis dapat kembali dicapai menggunakan ComplexTypeAttribute:
[ComplexType]
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
[ComplexType]
public record PhoneNumber(int CountryCode, long Number);
[ComplexType]
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
public required PhoneNumber MobilePhone { get; init; }
}
Atau di OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>(
b =>
{
b.ComplexProperty(
e => e.Contact,
b =>
{
b.ComplexProperty(e => e.Address);
b.ComplexProperty(e => e.HomePhone);
b.ComplexProperty(e => e.WorkPhone);
b.ComplexProperty(e => e.MobilePhone);
});
});
modelBuilder.Entity<Order>(
b =>
{
b.ComplexProperty(e => e.ContactPhone);
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Kueri
Properti jenis kompleks pada jenis entitas diperlakukan seperti properti non-navigasi lainnya dari jenis entitas. Ini berarti bahwa mereka selalu dimuat ketika jenis entitas dimuat. Ini juga berlaku untuk setiap properti jenis kompleks berlapis. Misalnya, mengkueri untuk pelanggan:
var customer = await context.Customers.FirstAsync(e => e.Id == customerId);
Diterjemahkan ke SQL berikut saat menggunakan SQL Server:
SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country],
[c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode],
[c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode],
[c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__customerId_0
Perhatikan dua hal dari SQL ini:
- Semuanya dikembalikan untuk mengisi pelanggan dan semua jenis berlapis
Contact
,Address
, danPhoneNumber
kompleks. - Semua nilai jenis kompleks disimpan sebagai kolom dalam tabel untuk jenis entitas. Jenis kompleks tidak pernah dipetakan ke tabel terpisah.
Proyeksi
Jenis kompleks dapat diproyeksikan dari kueri. Misalnya, memilih hanya alamat pengiriman dari pesanan:
var shippingAddress = await context.Orders
.Where(e => e.Id == orderId)
.Select(e => e.ShippingAddress)
.SingleAsync();
Ini diterjemahkan ke yang berikut ini saat menggunakan SQL Server:
SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1],
[o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[Id] = @__orderId_0
Perhatikan bahwa proyeksi jenis kompleks tidak dapat dilacak, karena objek jenis kompleks tidak memiliki identitas untuk digunakan untuk pelacakan.
Gunakan dalam predikat
Anggota jenis kompleks dapat digunakan dalam predikat. Misalnya, menemukan semua pesanan yang masuk ke kota tertentu:
var city = "Walpole St Peter";
var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync();
Yang diterjemahkan ke SQL berikut di SQL Server:
SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].[BillingAddress_City], [o].[BillingAddress_Country],
[o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].[BillingAddress_PostCode],
[o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].[ShippingAddress_City],
[o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].[ShippingAddress_Line2],
[o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[ShippingAddress_City] = @__city_0
Instans jenis kompleks penuh juga dapat digunakan dalam predikat. Misalnya, menemukan semua pelanggan dengan nomor telepon tertentu:
var phoneNumber = new PhoneNumber(44, 7777555777);
var customersWithNumber = await context.Customers
.Where(
e => e.Contact.MobilePhone == phoneNumber
|| e.Contact.WorkPhone == phoneNumber
|| e.Contact.HomePhone == phoneNumber)
.ToListAsync();
Ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:
SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], [c].[Contact_Address_Line1],
[c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].[Contact_HomePhone_CountryCode],
[c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], [c].[Contact_MobilePhone_Number],
[c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE ([c].[Contact_MobilePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_MobilePhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_WorkPhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_WorkPhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_HomePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_HomePhone_Number] = @__entity_equality_phoneNumber_0_Number)
Perhatikan bahwa kesetaraan dilakukan dengan memperluas setiap anggota dari jenis kompleks. Ini selaras dengan jenis kompleks yang tidak memiliki kunci untuk identitas dan karenanya instans jenis kompleks sama dengan instans jenis kompleks lainnya jika dan hanya jika semua anggota mereka sama. Ini juga selaras dengan kesetaraan yang ditentukan oleh .NET untuk jenis rekaman.
Manipulasi nilai jenis kompleks
EF8 menyediakan akses ke informasi pelacakan seperti nilai saat ini dan asli dari jenis kompleks dan apakah nilai properti telah dimodifikasi atau belum. Jenis kompleks API adalah ekstensi dari API pelacakan perubahan yang sudah digunakan untuk jenis entitas.
Metode ComplexProperty
EntityEntry mengembalikan entri untuk seluruh objek kompleks. Misalnya, untuk mendapatkan nilai saat ini dari Order.BillingAddress
:
var billingAddress = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.CurrentValue;
Panggilan ke Property
dapat ditambahkan untuk mengakses properti jenis kompleks. Misalnya untuk mendapatkan nilai saat ini hanya dari kode posting penagihan:
var postCode = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.Property(e => e.PostCode)
.CurrentValue;
Jenis kompleks berlapis diakses menggunakan panggilan berlapis ke ComplexProperty
. Misalnya, untuk mendapatkan kota dari bersarang Address
Contact
pada Customer
:
var currentCity = context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.City)
.CurrentValue;
Metode lain tersedia untuk membaca dan mengubah status. Misalnya, PropertyEntry.IsModified dapat digunakan untuk mengatur properti dari jenis kompleks sebagai dimodifikasi:
context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.PostCode)
.IsModified = true;
Batasan saat ini
Jenis kompleks mewakili investasi yang signifikan di seluruh tumpukan EF. Kami tidak dapat membuat semuanya berfungsi dalam rilis ini, tetapi kami berencana untuk menutup beberapa celah dalam rilis mendatang. Pastikan untuk memilih (👍) pada masalah GitHub yang sesuai jika memperbaiki salah satu batasan ini penting bagi Anda.
Batasan jenis kompleks di EF8 meliputi:
- Mendukung kumpulan jenis kompleks. (Masalah #31237)
- Perbolehkan properti tipe kompleks menjadi null. (Masalah #31376)
- Memetakan properti jenis kompleks ke kolom JSON. (Masalah #31252)
- Injeksi konstruktor untuk jenis kompleks. (Masalah #31621)
- Tambahkan dukungan data benih untuk jenis kompleks. (Masalah #31254)
- Petakan properti jenis kompleks untuk penyedia Cosmos. (Masalah #31253)
- Terapkan jenis kompleks untuk database dalam memori. (Masalah #31464)
Koleksi primitif
Pertanyaan persisten saat menggunakan database relasional adalah apa yang harus dilakukan dengan kumpulan jenis primitif; yaitu, daftar atau array bilangan bulat, tanggal/waktu, string, dan sebagainya. Jika Anda menggunakan PostgreSQL, maka mudah untuk menyimpan hal-hal ini menggunakan jenis array bawaan PostgreSQL. Untuk database lain, ada dua pendekatan umum:
- Buat tabel dengan kolom untuk nilai jenis primitif dan kolom lain untuk bertindak sebagai kunci asing yang menautkan setiap nilai ke pemilik koleksinya.
- Serialisasi koleksi primitif ke dalam beberapa jenis kolom yang ditangani oleh database--misalnya, serialisasi ke dan dari string.
Opsi pertama memiliki keuntungan dalam banyak situasi --kita akan melihat sekilas di akhir bagian ini. Namun, ini bukan representasi alami dari data dalam model, dan jika apa yang Benar-benar Anda miliki adalah kumpulan jenis primitif, maka opsi kedua bisa lebih efektif.
Dimulai dengan Pratinjau 4, EF8 sekarang menyertakan dukungan bawaan untuk opsi kedua, menggunakan JSON sebagai format serialisasi. JSON bekerja dengan baik untuk ini karena database relasional modern mencakup mekanisme bawaan untuk mengkueri dan memanipulasi JSON, sehingga kolom JSON dapat, secara efektif, diperlakukan sebagai tabel ketika diperlukan, tanpa overhead benar-benar membuat tabel itu. Mekanisme yang sama ini memungkinkan JSON untuk diteruskan dalam parameter dan kemudian digunakan dengan cara yang sama dengan parameter bernilai tabel dalam kueri--lebih lanjut tentang ini nanti.
Tip
Kode yang ditampilkan di sini berasal dari PrimitiveCollectionsSample.cs.
Properti koleksi primitif
EF Core dapat memetakan properti apa pun IEnumerable<T>
, di mana T
adalah jenis primitif, ke kolom JSON dalam database. Ini dilakukan oleh konvensi untuk properti publik yang memiliki getter dan setter. Misalnya, semua properti dalam jenis entitas berikut dipetakan ke kolom JSON menurut konvensi:
public class PrimitiveCollections
{
public IEnumerable<int> Ints { get; set; }
public ICollection<string> Strings { get; set; }
public IList<DateOnly> Dates { get; set; }
public uint[] UnsignedInts { get; set; }
public List<bool> Booleans { get; set; }
public List<Uri> Urls { get; set; }
}
Catatan
Apa yang kita maksud dengan "jenis primitif" dalam konteks ini? Pada dasarnya, sesuatu yang diketahui penyedia database cara memetakan, menggunakan semacam konversi nilai jika perlu. Misalnya, dalam jenis entitas di atas, jenis int
, , string
DateTime
, DateOnly
dan bool
semuanya ditangani tanpa konversi oleh penyedia database. SQL Server tidak memiliki dukungan asli untuk ints atau URI yang tidak ditandatangani, tetapi uint
dan Uri
masih diperlakukan sebagai jenis primitif karena ada pengonversi nilai bawaan untuk jenis ini.
Secara default, EF Core menggunakan jenis kolom string Unicode yang tidak dibatasi untuk menahan JSON, karena ini melindungi dari kehilangan data dengan koleksi besar. Namun, pada beberapa sistem database, seperti SQL Server, menentukan panjang maksimum untuk string dapat meningkatkan performa. Ini, bersama dengan konfigurasi kolom lainnya, dapat dilakukan dengan cara normal. Contohnya:
modelBuilder
.Entity<PrimitiveCollections>()
.Property(e => e.Booleans)
.HasMaxLength(1024)
.IsUnicode(false);
Atau, menggunakan atribut pemetaan:
[MaxLength(2500)]
[Unicode(false)]
public uint[] UnsignedInts { get; set; }
Konfigurasi kolom default dapat digunakan untuk semua properti dari jenis tertentu menggunakan konfigurasi model pra-konvensi. Contohnya:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<List<DateOnly>>()
.AreUnicode(false)
.HaveMaxLength(4000);
}
Kueri dengan koleksi primitif
Mari kita lihat beberapa kueri yang memanfaatkan koleksi jenis primitif. Untuk ini, kita akan memerlukan model sederhana dengan dua jenis entitas. Yang pertama mewakili rumah publik Inggris, atau "pub":
public class Pub
{
public Pub(string name, string[] beers)
{
Name = name;
Beers = beers;
}
public int Id { get; set; }
public string Name { get; set; }
public string[] Beers { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
}
Jenisnya Pub
berisi dua koleksi primitif:
Beers
adalah array string yang mewakili merek bir yang tersedia di pub.DaysVisited
adalah daftar tanggal di mana pub dikunjungi.
Tip
Dalam aplikasi nyata, mungkin akan lebih masuk akal untuk membuat jenis entitas untuk bir, dan memiliki tabel untuk bir. Kami menampilkan koleksi primitif di sini untuk menggambarkan cara kerjanya. Tapi ingat, hanya karena Anda dapat memodelkan sesuatu sebagai koleksi primitif tidak berarti bahwa Anda harus melakukannya.
Jenis entitas kedua mewakili perjalanan anjing di pedesaan Inggris:
public class DogWalk
{
public DogWalk(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public Terrain Terrain { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
public Pub ClosestPub { get; set; } = null!;
}
public enum Terrain
{
Forest,
River,
Hills,
Village,
Park,
Beach,
}
Seperti Pub
, DogWalk
juga berisi koleksi tanggal yang dikunjungi, dan tautan ke pub terdekat karena, Anda tahu, kadang-kadang anjing membutuhkan saus bir setelah berjalan-jalan.
Menggunakan model ini, kueri pertama yang akan kita lakukan adalah kueri sederhana Contains
untuk menemukan semua panduan dengan salah satu dari beberapa medan yang berbeda:
var terrains = new[] { Terrain.River, Terrain.Beach, Terrain.Park };
var walksWithTerrain = await context.Walks
.Where(e => terrains.Contains(e.Terrain))
.Select(e => e.Name)
.ToListAsync();
Ini sudah diterjemahkan oleh versi EF Core saat ini dengan menginlining nilai yang akan dicari. Misalnya, saat menggunakan SQL Server:
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE [w].[Terrain] IN (1, 5, 4)
Namun, strategi ini tidak berfungsi dengan baik dengan penembolokan kueri database; lihat Mengumumkan Pratinjau EF8 4 di Blog .NET untuk diskusi tentang masalah ini.
Penting
Inlining nilai di sini dilakukan sed sehingga tidak ada kemungkinan serangan injeksi SQL. Perubahan untuk menggunakan JSON yang dijelaskan di bawah ini adalah tentang performa, dan tidak ada hubungannya dengan keamanan.
Untuk EF Core 8, defaultnya sekarang adalah meneruskan daftar medan sebagai parameter tunggal yang berisi koleksi JSON. Contohnya:
@__terrains_0='[1,5,4]'
Kueri kemudian menggunakan OpenJson
di SQL Server:
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__terrains_0) AS [t]
WHERE CAST([t].[value] AS int) = [w].[Terrain])
Atau json_each
di SQLite:
SELECT "w"."Name"
FROM "Walks" AS "w"
WHERE EXISTS (
SELECT 1
FROM json_each(@__terrains_0) AS "t"
WHERE "t"."value" = "w"."Terrain")
Catatan
OpenJson
hanya tersedia di SQL Server 2016 (tingkat kompatibilitas 130) dan yang lebih baru. Anda dapat memberi tahu SQL Server bahwa Anda menggunakan versi yang lebih lama dengan mengonfigurasi tingkat kompatibilitas sebagai bagian UseSqlServer
dari . Contohnya:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(
@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120));
Mari kita coba jenis Contains
kueri yang berbeda. Dalam hal ini, kita akan mencari nilai koleksi parameter di kolom . Misalnya, pub apa pun yang menimbun Heineken:
var beer = "Heineken";
var pubsWithHeineken = await context.Pubs
.Where(e => e.Beers.Contains(beer))
.Select(e => e.Name)
.ToListAsync();
Dokumentasi yang ada dari Apa yang Baru di EF7 menyediakan informasi terperinci tentang pemetaan, kueri, dan pembaruan JSON. Dokumentasi ini sekarang juga berlaku untuk SQLite.
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b]
WHERE [b].[value] = @__beer_0)
OpenJson
sekarang digunakan untuk mengekstrak nilai dari kolom JSON sehingga setiap nilai dapat dicocokkan dengan parameter yang diteruskan.
Kita dapat menggabungkan penggunaan OpenJson
pada parameter dengan OpenJson
pada kolom . Misalnya, untuk menemukan pub yang menimbun salah satu dari berbagai lager:
var beers = new[] { "Carling", "Heineken", "Stella Artois", "Carlsberg" };
var pubsWithLager = await context.Pubs
.Where(e => beers.Any(b => e.Beers.Contains(b)))
.Select(e => e.Name)
.ToListAsync();
Ini diterjemahkan ke yang berikut ini di SQL Server:
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__beers_0) AS [b]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b0]
WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL)))
Nilai @__beers_0
parameter di sini adalah ["Carling","Heineken","Stella Artois","Carlsberg"]
.
Mari kita lihat kueri yang menggunakan kolom yang berisi kumpulan tanggal. Misalnya, untuk menemukan pub yang dikunjungi tahun ini:
var thisYear = DateTime.Now.Year;
var pubsVisitedThisYear = await context.Pubs
.Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
.Select(e => e.Name)
.ToListAsync();
Ini diterjemahkan ke yang berikut ini di SQL Server:
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d]
WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)
Perhatikan bahwa kueri menggunakan fungsi DATEPART
khusus tanggal di sini karena EF tahu bahwa koleksi primitif berisi tanggal. Mungkin tidak tampak seperti itu, tetapi ini sebenarnya benar-benar penting. Karena EF tahu apa yang ada dalam koleksi, EF dapat menghasilkan SQL yang sesuai untuk menggunakan nilai yang diketik dengan parameter, fungsi, kolom lain, dll.
Mari kita gunakan koleksi tanggal lagi, kali ini untuk memesan dengan tepat untuk jenis dan nilai proyek yang diekstrak dari koleksi. Misalnya, mari kita daftar pub dalam urutan bahwa mereka pertama kali dikunjungi, dan dengan tanggal pertama dan terakhir setiap pub dikunjungi:
var pubsVisitedInOrder = await context.Pubs
.Select(e => new
{
e.Name,
FirstVisited = e.DaysVisited.OrderBy(v => v).First(),
LastVisited = e.DaysVisited.OrderByDescending(v => v).First(),
})
.OrderBy(p => p.FirstVisited)
.ToListAsync();
Ini diterjemahkan ke yang berikut ini di SQL Server:
SELECT [p].[Name], (
SELECT TOP(1) CAST([d0].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d0]
ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], (
SELECT TOP(1) CAST([d1].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d1]
ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited]
FROM [Pubs] AS [p]
ORDER BY (
SELECT TOP(1) CAST([d].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d]
ORDER BY CAST([d].[value] AS date))
Dan akhirnya, seberapa sering kita akhirnya mengunjungi pub terdekat ketika mengajak anjing berjalan-jalan? Mari kita cari tahu:
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
Ini diterjemahkan ke yang berikut ini di SQL Server:
SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], (
SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
Dan mengungkapkan data berikut:
The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks.
The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks.
The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks.
The White Swan was visited 7 times in 9 "Woodnewton" walks.
The Eltisley was visited 6 times in 8 "Eltisley" walks.
Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks.
Farr Bay Inn was visited 7 times in 9 "Newlands" walks.
Sepertinya bir dan anjing berjalan adalah kombinasi yang menang!
Koleksi primitif dalam dokumen JSON
Dalam semua contoh di atas, kolom untuk koleksi primitif berisi JSON. Namun, ini tidak sama dengan memetakan jenis entitas yang dimiliki ke kolom yang berisi dokumen JSON, yang diperkenalkan di EF7. Tapi bagaimana jika dokumen JSON itu sendiri berisi koleksi primitif? Nah, semua kueri di atas masih berfungsi dengan cara yang sama! Misalnya, bayangkan kita memindahkan hari-hari data yang dikunjungi ke dalam jenis Visits
milik yang dipetakan ke dokumen JSON:
public class Pub
{
public Pub(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public BeerData Beers { get; set; } = null!;
public Visits Visits { get; set; } = null!;
}
public class Visits
{
public string? LocationTag { get; set; }
public List<DateOnly> DaysVisited { get; set; } = null!;
}
Tip
Kode yang ditampilkan di sini berasal dari PrimitiveCollectionsInJsonSample.cs.
Kita sekarang dapat menjalankan variasi kueri akhir kita yang, kali ini, mengekstrak data dari dokumen JSON, termasuk kueri ke dalam koleksi primitif yang terkandung dalam dokumen:
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
WalkLocationTag = w.Visits.LocationTag,
PubLocationTag = w.ClosestPub.Visits.LocationTag,
Count = w.Visits.DaysVisited.Count(v => w.ClosestPub.Visits.DaysVisited.Contains(v)),
TotalCount = w.Visits.DaysVisited.Count
}).ToListAsync();
Ini diterjemahkan ke yang berikut ini di SQL Server:
SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
Dan untuk kueri serupa saat menggunakan SQLite:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", (
SELECT COUNT(*)
FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d"
WHERE EXISTS (
SELECT 1
FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0"
WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
Tip
Perhatikan bahwa pada SQLite EF Core sekarang memanfaatkan ->>
operator, menghasilkan kueri yang lebih mudah dibaca dan sering kali lebih berkinerja.
Memetakan koleksi primitif ke tabel
Kami menyebutkan di atas bahwa opsi lain untuk koleksi primitif adalah memetakannya ke tabel yang berbeda. Dukungan kelas satu untuk ini dilacak oleh Masalah #25163; pastikan untuk memilih masalah ini jika penting bagi Anda. Sampai ini diimplementasikan, pendekatan terbaik adalah membuat jenis pembungkusan untuk primitif. Misalnya, mari kita buat jenis untuk Beer
:
[Owned]
public class Beer
{
public Beer(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Perhatikan bahwa jenis hanya membungkus nilai primitif --tidak memiliki kunci primer atau kunci asing apa pun yang ditentukan. Jenis ini kemudian dapat digunakan di Pub
kelas :
public class Pub
{
public Pub(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public List<Beer> Beers { get; set; } = new();
public List<DateOnly> DaysVisited { get; private set; } = new();
}
EF sekarang akan membuat Beer
tabel, mensintesis kunci primer dan kolom kunci asing kembali ke Pubs
tabel. Misalnya, di SQL Server:
CREATE TABLE [Beer] (
[PubId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]),
CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE
Penyempurnaan pemetaan kolom JSON
EF8 mencakup penyempurnaan dukungan pemetaan kolom JSON yang diperkenalkan di EF7.
Tip
Kode yang ditampilkan di sini berasal dari JsonColumnsSample.cs.
Menerjemahkan akses elemen ke dalam array JSON
EF8 mendukung pengindeksan dalam array JSON saat menjalankan kueri. Misalnya, kueri berikut memeriksa apakah dua pembaruan pertama dibuat sebelum tanggal tertentu.
var cutoff = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(365));
var updatedPosts = await context.Posts
.Where(
p => p.Metadata!.Updates[0].UpdatedOn < cutoff
&& p.Metadata!.Updates[1].UpdatedOn < cutoff)
.ToListAsync();
Ini diterjemahkan ke dalam SQL berikut saat menggunakan SQL Server:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0
AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0
Catatan
Kueri ini akan berhasil meskipun postingan tertentu tidak memiliki pembaruan apa pun, atau hanya memiliki satu pembaruan. Dalam kasus seperti itu, JSON_VALUE
pengembalian NULL
dan predikat tidak cocok.
Pengindeksan ke dalam array JSON juga dapat digunakan untuk memproyeksikan elemen dari array ke hasil akhir. Misalnya, kueri berikut memproyeksikan UpdatedOn
tanggal untuk pembaruan pertama dan kedua dari setiap postingan.
var postsAndRecentUpdatesNullable = await context.Posts
.Select(p => new
{
p.Title,
LatestUpdate = (DateOnly?)p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = (DateOnly?)p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();
Ini diterjemahkan ke dalam SQL berikut saat menggunakan SQL Server:
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]
Seperti disebutkan di atas, JSON_VALUE
mengembalikan null jika elemen array tidak ada. Ini ditangani dalam kueri dengan mentransmisikan nilai yang diproyeksikan ke nullable DateOnly
. Alternatif untuk transmisi nilai adalah memfilter hasil kueri sehingga JSON_VALUE
tidak akan pernah mengembalikan null. Contohnya:
var postsAndRecentUpdates = await context.Posts
.Where(p => p.Metadata!.Updates[0].UpdatedOn != null
&& p.Metadata!.Updates[1].UpdatedOn != null)
.Select(p => new
{
p.Title,
LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();
Ini diterjemahkan ke dalam SQL berikut saat menggunakan SQL Server:
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]
WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL)
AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL)
Menerjemahkan kueri ke dalam koleksi yang disematkan
EF8 mendukung kueri terhadap koleksi jenis primitif (dibahas di atas) dan non-primitif yang disematkan dalam dokumen JSON. Misalnya, kueri berikut mengembalikan semua postingan dengan salah satu daftar istilah pencarian arbitrer:
var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search #8", "Search #13", "Search #21", "Search #34" };
var postsWithSearchTerms = await context.Posts
.Where(post => post.Metadata!.TopSearches.Any(s => searchTerms.Contains(s.Term)))
.ToListAsync();
Ini diterjemahkan ke dalam SQL berikut saat menggunakan SQL Server:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM OPENJSON([p].[Metadata], '$.TopSearches') WITH (
[Count] int '$.Count',
[Term] nvarchar(max) '$.Term'
) AS [t]
WHERE EXISTS (
SELECT 1
FROM OPENJSON(@__searchTerms_0) WITH ([value] nvarchar(max) '$') AS [s]
WHERE [s].[value] = [t].[Term]))
Kolom JSON untuk SQLite
EF7 memperkenalkan dukungan untuk pemetaan ke kolom JSON saat menggunakan Azure SQL/SQL Server. EF8 memperluas dukungan ini ke database SQLite. Adapun dukungan SQL Server, ini termasuk:
- Pemetaan agregat yang dibangun dari jenis .NET ke dokumen JSON yang disimpan dalam kolom SQLite
- Kueri ke dalam kolom JSON, seperti pemfilteran dan pengurutan menurut elemen dokumen
- Kueri yang memproyeksikan elemen dari dokumen JSON menjadi hasil
- Memperbarui dan menyimpan perubahan pada dokumen JSON
Dokumentasi yang ada dari Apa yang Baru di EF7 menyediakan informasi terperinci tentang pemetaan, kueri, dan pembaruan JSON. Dokumentasi ini sekarang juga berlaku untuk SQLite.
Tip
Kode yang ditampilkan dalam dokumentasi EF7 telah diperbarui untuk juga dijalankan di SQLite dapat ditemukan di JsonColumnsSample.cs.
Kueri ke dalam kolom JSON
Kueri ke dalam kolom JSON di SQLite menggunakan fungsi .json_extract
Misalnya, kueri "penulis di Chigley" dari dokumentasi yang dirujuk di atas:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
Diterjemahkan ke SQL berikut saat menggunakan SQLite:
SELECT "a"."Id", "a"."Name", "a"."Contact"
FROM "Authors" AS "a"
WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'
Memperbarui kolom JSON
Untuk pembaruan, EF menggunakan json_set
fungsi pada SQLite. Misalnya, saat memperbarui satu properti dalam dokumen:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
EF menghasilkan parameter berikut:
info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']
Yang menggunakan json_set
fungsi pada SQLite:
UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]'))
WHERE "Id" = @p1
RETURNING 1;
HierarchyId di .NET dan EF Core
Azure SQL dan SQL Server memiliki jenis data khusus yang disebut hierarchyid
yang digunakan untuk menyimpan data hierarkis. Dalam hal ini, "data hierarkis" pada dasarnya berarti data yang membentuk struktur pohon, di mana setiap item dapat memiliki induk dan/atau anak. Contoh data tersebut adalah:
- Struktur organisasi
- Sistem file
- Sekumpulan tugas dalam proyek
- Taksonomi istilah bahasa
- Grafik tautan antar halaman Web
Database kemudian dapat menjalankan kueri terhadap data ini menggunakan struktur hierarkisnya. Misalnya, kueri dapat menemukan leluhur dan dependen item tertentu, atau menemukan semua item pada kedalaman tertentu dalam hierarki.
Dukungan di .NET dan EF Core
Dukungan resmi untuk jenis SQL Server hierarchyid
baru-baru ini datang ke platform .NET modern (yaitu ".NET Core"). Dukungan ini dalam bentuk paket NuGet Microsoft.SqlServer.Types , yang membawa jenis khusus SQL Server tingkat rendah. Dalam hal ini, jenis tingkat rendah disebut SqlHierarchyId
.
Pada tingkat berikutnya, paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions baru telah diperkenalkan, yang mencakup jenis tingkat HierarchyId
yang lebih tinggi yang dimaksudkan untuk digunakan dalam jenis entitas.
Tip
Jenis HierarchyId
ini lebih idiomatik dengan norma .NET daripada SqlHierarchyId
, yang sebaliknya dimodelkan setelah bagaimana jenis .NET Framework dihosting di dalam mesin database SQL Server. HierarchyId
dirancang untuk bekerja dengan EF Core, tetapi juga dapat digunakan di luar EF Core di aplikasi lain. Paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions
ini tidak mereferensikan paket lain, sehingga berdampak minimal pada ukuran dan dependensi aplikasi yang disebarkan.
Penggunaan HierarchyId
untuk fungsionalitas EF Core seperti kueri dan pembaruan memerlukan paket Microsoft.EntityFrameworkCore.SqlServer.HierarchyId . Paket ini membawa dan Microsoft.EntityFrameworkCore.SqlServer.Abstractions
Microsoft.SqlServer.Types
sebagai dependensi transitif, dan begitu juga seringkali satu-satunya paket yang diperlukan. Setelah paket diinstal, penggunaan HierarchyId
diaktifkan dengan memanggil UseHierarchyId
sebagai bagian dari panggilan aplikasi ke UseSqlServer
. Contoh:
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
Catatan
Dukungan tidak resmi untuk hierarchyid
di EF Core telah tersedia selama bertahun-tahun melalui paket EntityFrameworkCore.SqlServer.HierarchyId . Paket ini telah dipertahankan sebagai kolaborasi antara komunitas dan tim EF. Sekarang setelah ada dukungan resmi untuk hierarchyid
di .NET, kode dari formulir paket komunitas ini, dengan izin kontributor asli, dasar untuk paket resmi yang dijelaskan di sini. Banyak terima kasih kepada semua orang yang terlibat selama bertahun-tahun, termasuk @aljones, @cutig3r, @huan086, @kmataru, @mehdihaghshenas, dan @vyrotek
Hierarki pemodelan
Jenis HierarchyId
dapat digunakan untuk properti jenis entitas. Misalnya, asumsikan kita ingin memodelkan pohon keluarga paternal dari beberapa paruh fiksi. Dalam jenis entitas untuk Halfling
, HierarchyId
properti dapat digunakan untuk menemukan setiap setengah di pohon keluarga.
public class Halfling
{
public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null)
{
PathFromPatriarch = pathFromPatriarch;
Name = name;
YearOfBirth = yearOfBirth;
}
public int Id { get; private set; }
public HierarchyId PathFromPatriarch { get; set; }
public string Name { get; set; }
public int? YearOfBirth { get; set; }
}
Tip
Kode yang ditunjukkan di sini dan dalam contoh di bawah ini berasal dari HierarchyIdSample.cs.
Tip
Jika diinginkan, HierarchyId
cocok untuk digunakan sebagai jenis properti kunci.
Dalam hal ini, pohon keluarga berakar pada patriarki keluarga. Setiap halfling dapat dilacak dari patriarki di bawah pohon menggunakan propertinya PathFromPatriarch
. SQL Server menggunakan format biner yang ringkas untuk jalur ini, tetapi umum untuk mengurai ke dan dari representasi string yang dapat dibaca manusia saat bekerja dengan kode. Dalam representasi ini, posisi di setiap tingkat dipisahkan oleh /
karakter. Misalnya, pertimbangkan pohon keluarga dalam diagram di bawah ini:
Di pohon ini:
- Balbo berada di akar pohon, diwakili oleh
/
. - Balbo memiliki lima anak, yang diwakili oleh
/1/
, ,/2/
/3/
,/4/
, dan/5/
. - Anak pertama Balbo, Mungo, juga memiliki lima anak, yang diwakili oleh
/1/1/
, ,/1/2/
/1/3/
,/1/4/
, dan/1/5/
. Perhatikan bahwaHierarchyId
untuk Balbo (/1/
) adalah awalan untuk semua anak-anaknya. - Demikian pula, anak ketiga Balbo, Ponto, memiliki dua anak, diwakili oleh
/3/1/
dan/3/2/
. Sekali lagi masing-masing anak ini diawali olehHierarchyId
untuk Ponto, yang diwakili sebagai/3/
. - Dan sebagainya di bawah pohon ...
Kode berikut menyisipkan pohon keluarga ini ke dalam database menggunakan EF Core:
await AddRangeAsync(
new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));
await SaveChangesAsync();
Tip
Jika diperlukan, nilai desimal dapat digunakan untuk membuat simpul baru di antara dua simpul yang ada. Misalnya, /3/2.5/2/
berjalan antara /3/2/2/
dan /3/3/2/
.
Mengkueri hierarki
HierarchyId
mengekspos beberapa metode yang dapat digunakan dalam kueri LINQ.
Metode | Deskripsi |
---|---|
GetAncestor(int n) |
Mendapatkan tingkat simpul n ke atas pohon hierarkis. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
Mendapatkan nilai node turunan yang lebih besar dari child1 dan kurang dari child2 . |
GetLevel() |
Mendapatkan tingkat simpul ini di pohon hierarkis. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Mendapatkan nilai yang mewakili lokasi simpul baru yang memiliki jalur dari newRoot sama dengan jalur dari oldRoot ke ini, secara efektif memindahkan ini ke lokasi baru. |
IsDescendantOf(HierarchyId? parent) |
Mendapatkan nilai yang menunjukkan apakah simpul ini adalah turunan dari parent . |
Selain itu, operator ==
, , !=
<
, <=
, >
dan >=
dapat digunakan.
Berikut ini adalah contoh penggunaan metode ini dalam kueri LINQ.
Mendapatkan entitas pada tingkat tertentu di pohon
Kueri berikut menggunakan GetLevel
untuk mengembalikan semua halfling pada tingkat tertentu di pohon keluarga:
var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
Ini diterjemahkan ke SQL berikut:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
Menjalankan ini dalam perulangan kita bisa mendapatkan halfling untuk setiap generasi:
Generation 0: Balbo
Generation 1: Mungo, Pansy, Ponto, Largo, Lily
Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
Generation 5: Angelica
Mendapatkan leluhur langsung entitas
Kueri berikut menggunakan GetAncestor
untuk menemukan leluhur langsung dari halfling, mengingat nama halfling tersebut:
async Task<Halfling?> FindDirectAncestor(string name)
=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name == name).PathFromPatriarch
.GetAncestor(1));
Ini diterjemahkan ke SQL berikut:
SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch] = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0).GetAncestor(1)
Menjalankan kueri ini untuk halfling "Bilbo" mengembalikan "Bungo".
Mendapatkan turunan langsung entitas
Kueri berikut juga menggunakan GetAncestor
, tetapi kali ini untuk menemukan keturunan langsung dari halfling, mengingat nama halfling itu:
IQueryable<Halfling> FindDirectDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);
Ini diterjemahkan ke SQL berikut:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0)
Menjalankan kueri ini untuk halfling "Mungo" mengembalikan "Bungo", "Belba", "Longo", dan "Linda".
Mendapatkan semua leluhur entitas
GetAncestor
berguna untuk mencari ke atas atau ke bawah satu tingkat, atau, memang, jumlah tingkat yang ditentukan. Di sisi lain, IsDescendantOf
berguna untuk menemukan semua leluhur atau dependen. Misalnya, kueri berikut menggunakan IsDescendantOf
untuk menemukan semua leluhur dari halfling, mengingat nama halfling itu:
IQueryable<Halfling> FindAllAncestors(string name)
=> context.Halflings.Where(
ancestor => context.Halflings
.Single(
descendent =>
descendent.Name == name
&& ancestor.Id != descendent.Id)
.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel());
Penting
IsDescendantOf
mengembalikan true untuk dirinya sendiri, itulah sebabnya difilter dalam kueri di atas.
Ini diterjemahkan ke SQL berikut:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
Menjalankan kueri ini untuk halfling "Bilbo" mengembalikan "Bungo", "Mungo", dan "Balbo".
Mendapatkan semua keturunan entitas
Kueri berikut juga menggunakan IsDescendantOf
, tetapi kali ini untuk semua keturunan dari halfling, mengingat nama halfling itu:
IQueryable<Halfling> FindAllDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings
.Single(
ancestor =>
ancestor.Name == name
&& descendent.Id != ancestor.Id)
.PathFromPatriarch))
.OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());
Ini diterjemahkan ke SQL berikut:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].IsDescendantOf((
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel()
Menjalankan kueri ini untuk halfling "Mungo" mengembalikan "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho", dan "Poppy".
Menemukan leluhur umum
Salah satu pertanyaan paling umum yang diajukan tentang pohon keluarga khusus ini adalah, "siapa nenek moyang umum Frodo dan Bilbo?" Kita dapat menggunakan IsDescendantOf
untuk menulis kueri seperti itu:
async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)
=> await context.Halflings
.Where(
ancestor => first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
&& second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel())
.FirstOrDefaultAsync();
Ini diterjemahkan ke SQL berikut:
SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
Menjalankan kueri ini dengan "Bilbo" dan "Frodo" memberi tahu kita bahwa leluhur umum mereka adalah "Balbo".
Memperbarui hierarki
Mekanisme pelacakan perubahan normal dan SaveChanges dapat digunakan untuk memperbarui hierarchyid
kolom.
Mengasuh ulang sub-hierarki
Misalnya, saya yakin kita semua ingat skandal SR 1752 (alias "LongoGate") ketika pengujian DNA mengungkapkan bahwa Longo sebenarnya bukan putra Mungo, tetapi sebenarnya putra Ponto! Satu fallout dari skandal ini adalah bahwa pohon keluarga perlu ditulis ulang. Secara khusus, Longo dan semua keturunannya perlu diasuh kembali dari Mungo ke Ponto. GetReparentedValue
dapat digunakan untuk melakukan ini. Misalnya, "Longo" pertama dan semua keturunannya dikueri:
var longoAndDescendents = await context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
.ToListAsync();
Kemudian GetReparentedValue
digunakan untuk memperbarui HierarchyId
untuk Longo dan setiap keturunan, diikuti dengan panggilan ke SaveChangesAsync
:
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
Ini menghasilkan pembaruan database berikut:
SET NOCOUNT ON;
UPDATE [Halflings] SET [PathFromPatriarch] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Halflings] SET [PathFromPatriarch] = @p2
OUTPUT 1
WHERE [Id] = @p3;
UPDATE [Halflings] SET [PathFromPatriarch] = @p4
OUTPUT 1
WHERE [Id] = @p5;
Menggunakan parameter ini:
@p1='9',
@p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
@p3='16',
@p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
@p5='23',
@p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)
Catatan
Nilai parameter untuk HierarchyId
properti dikirim ke database dalam format biner yang ringkas.
Setelah pembaruan, mengkueri keturunan "Mungo" mengembalikan "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco", dan "Poppy", sementara mengkueri keturunan "Ponto" mengembalikan "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony", dan "Angelica".
Kueri SQL mentah untuk jenis yang tidak dipetakan
EF7 memperkenalkan kueri SQL mentah yang mengembalikan jenis skalar. Ini ditingkatkan dalam EF8 untuk menyertakan kueri SQL mentah yang mengembalikan jenis CLR yang dapat dipetakan, tanpa menyertakan jenis tersebut dalam model EF.
Tip
Kode yang ditampilkan di sini berasal dari RawSqlSample.cs.
Kueri yang menggunakan jenis yang tidak dipetakan dijalankan menggunakan SqlQuery atau SqlQueryRaw. Yang pertama menggunakan interpolasi string untuk membuat parameter kueri, yang membantu memastikan bahwa semua nilai non-konstanta diparameterkan. Misalnya, pertimbangkan tabel database berikut:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Content] nvarchar(max) NOT NULL,
[PublishedOn] date NOT NULL,
[BlogId] int NOT NULL,
);
SqlQuery
dapat digunakan untuk mengkueri tabel ini dan mengembalikan instans jenis dengan properti yang BlogPost
sesuai dengan kolom dalam tabel:
Contohnya:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateOnly PublishedOn { get; set; }
public int BlogId { get; set; }
}
Contohnya:
var start = new DateOnly(2022, 1, 1);
var end = new DateOnly(2023, 1, 1);
var postsIn2022 =
await context.Database
.SqlQuery<BlogPost>($"SELECT * FROM Posts as p WHERE p.PublishedOn >= {start} AND p.PublishedOn < {end}")
.ToListAsync();
Kueri ini diparameterkan dan dijalankan sebagai:
SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1
Jenis yang digunakan untuk hasil kueri dapat berisi konstruksi pemetaan umum yang didukung oleh EF Core, seperti konstruktor berparameter dan atribut pemetaan. Contoh:
public class BlogPost
{
public BlogPost(string blogTitle, string content, DateOnly publishedOn)
{
BlogTitle = blogTitle;
Content = content;
PublishedOn = publishedOn;
}
public int Id { get; private set; }
[Column("Title")]
public string BlogTitle { get; set; }
public string Content { get; set; }
public DateOnly PublishedOn { get; set; }
public int BlogId { get; set; }
}
Catatan
Jenis yang digunakan dengan cara ini tidak memiliki kunci yang ditentukan dan tidak dapat memiliki hubungan dengan jenis lain. Jenis dengan hubungan harus dipetakan dalam model.
Jenis yang digunakan harus memiliki properti untuk setiap nilai dalam tataan hasil, tetapi tidak perlu mencocokkan tabel apa pun dalam database. Misalnya, jenis berikut hanya mewakili subset informasi untuk setiap postingan, dan menyertakan nama blog, yang berasal dari Blogs
tabel:
public class PostSummary
{
public string BlogName { get; set; } = null!;
public string PostTitle { get; set; } = null!;
public DateOnly? PublishedOn { get; set; }
}
Dan dapat dikueri menggunakan SqlQuery
dengan cara yang sama seperti sebelumnya:
var cutoffDate = new DateOnly(2022, 1, 1);
var summaries =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id
WHERE p.PublishedOn >= {cutoffDate}")
.ToListAsync();
Salah satu fitur SqlQuery
yang bagus adalah bahwa ia mengembalikan yang IQueryable
dapat terdiri dari menggunakan LINQ. Misalnya, klausa 'Di mana' dapat ditambahkan ke kueri di atas:
var summariesIn2022 =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
Ini dijalankan sebagai:
SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn]
FROM (
SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id
) AS [n]
WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2
Pada titik ini perlu diingat bahwa semua hal di atas dapat dilakukan sepenuhnya di LINQ tanpa perlu menulis SQL apa pun. Ini termasuk mengembalikan instans dari jenis yang tidak dipetakan seperti PostSummary
. Misalnya, kueri sebelumnya dapat ditulis dalam LINQ sebagai:
var summaries =
await context.Posts.Select(
p => new PostSummary
{
BlogName = p.Blog.Name,
PostTitle = p.Title,
PublishedOn = p.PublishedOn,
})
.Where(p => p.PublishedOn >= start && p.PublishedOn < end)
.ToListAsync();
Yang diterjemahkan ke SQL yang jauh lebih bersih:
SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn]
FROM [Posts] AS [p]
INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id]
WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1
Tip
EF dapat menghasilkan SQL yang lebih bersih ketika bertanggung jawab atas seluruh kueri daripada saat menyusun SQL yang disediakan pengguna karena, dalam kasus sebelumnya, semantik lengkap kueri tersedia untuk EF.
Sejauh ini, semua kueri telah dijalankan langsung terhadap tabel. SqlQuery
juga dapat digunakan untuk mengembalikan hasil dari tampilan tanpa memetakan jenis tampilan dalam model EF. Contohnya:
var summariesFromView =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM PostAndBlogSummariesView")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
Demikian juga, SqlQuery
dapat digunakan untuk hasil fungsi:
var summariesFromFunc =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
.Where(p => p.PublishedOn < end)
.ToListAsync();
Yang dikembalikan IQueryable
dapat terdiri ketika merupakan hasil dari tampilan atau fungsi, sama seperti hasil kueri tabel. Prosedur tersimpan juga dapat dijalankan menggunakan SqlQuery
, tetapi sebagian besar database tidak mendukung pembuatannya. Contohnya:
var summariesFromStoredProc =
await context.Database.SqlQuery<PostSummary>(
@$"exec GetRecentPostSummariesProc")
.ToListAsync();
Penyempurnaan untuk pemuatan malas
Pemuatan malas untuk kueri tanpa pelacakan
EF8 menambahkan dukungan untuk pemuatan navigasi yang malas pada entitas yang tidak dilacak oleh DbContext
. Ini berarti kueri tanpa pelacakan dapat diikuti dengan pemuatan navigasi yang malas pada entitas yang dikembalikan oleh kueri tanpa pelacakan.
Tip
Kode untuk contoh pemuatan malas yang ditunjukkan di bawah ini berasal dari LazyLoadingSample.cs.
Misalnya, pertimbangkan kueri tanpa pelacakan untuk blog:
var blogs = await context.Blogs.AsNoTracking().ToListAsync();
Jika Blog.Posts
dikonfigurasi untuk pemuatan malas, misalnya, menggunakan proksi pemuatan malas, maka mengakses Posts
akan menyebabkannya dimuat dari database:
Console.WriteLine();
Console.Write("Choose a blog: ");
if (int.TryParse(ReadLine(), out var blogId))
{
Console.WriteLine("Posts:");
foreach (var post in blogs[blogId - 1].Posts)
{
Console.WriteLine($" {post.Title}");
}
}
EF8 juga melaporkan apakah navigasi tertentu dimuat untuk entitas yang tidak dilacak oleh konteks atau tidak. Contohnya:
foreach (var blog in blogs)
{
if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
{
Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
}
}
Ada beberapa pertimbangan penting saat menggunakan pemuatan malas dengan cara ini:
- Pemuatan malas hanya akan berhasil sampai
DbContext
digunakan untuk mengkueri entitas dibuang. - Entitas yang dikueri dengan cara ini mempertahankan referensi ke mereka
DbContext
, meskipun tidak dilacak olehnya. Perawatan harus dilakukan untuk menghindari kebocoran memori jika instans entitas akan memiliki masa pakai yang lama. - Secara eksplisit melepaskan entitas dengan mengatur statusnya untuk
EntityState.Detached
memutuskan referensi keDbContext
pemuatan malas dan tidak akan berfungsi lagi. - Ingatlah bahwa semua pemuatan malas menggunakan I/O sinkron, karena tidak ada cara untuk mengakses properti dengan cara asinkron.
Pemuatan malas dari entitas yang tidak terlacak berfungsi untuk proksi pemuatan malas dan pemuatan malas tanpa proksi.
Pemuatan eksplisit dari entitas yang tidak terlacak
EF8 mendukung pemuatan navigasi pada entitas yang tidak terlacak bahkan ketika entitas atau navigasi tidak dikonfigurasi untuk pemuatan malas. Tidak seperti pemuatan malas, pemuatan eksplisit ini dapat dilakukan secara asinkron. Contohnya:
await context.Entry(blog).Collection(e => e.Posts).LoadAsync();
Menolak pemuatan malas untuk navigasi tertentu
EF8 memungkinkan konfigurasi navigasi tertentu untuk tidak malas memuat, bahkan ketika segala sesuatu yang lain disiapkan untuk melakukannya. Misalnya, untuk mengonfigurasi Post.Author
navigasi agar tidak malas memuat, lakukan hal berikut:
modelBuilder
.Entity<Post>()
.Navigation(p => p.Author)
.EnableLazyLoading(false);
Menonaktifkan Pemuatan Malas seperti ini berfungsi untuk proksi pemuatan malas dan pemuatan malas tanpa proksi.
Proksi pemuatan malas berfungsi dengan mengambil alih properti navigasi virtual. Dalam aplikasi EF6 klasik, sumber bug umum lupa membuat navigasi virtual, karena navigasi kemudian akan diam-diam tidak malas-beban. Oleh karena itu, proksi EF Core dilemparkan secara default ketika navigasi tidak virtual.
Ini dapat diubah di EF8 untuk ikut serta dalam perilaku EF6 klasik sehingga navigasi dapat dilakukan untuk tidak malas-beban hanya dengan membuat navigasi non-virtual. Keikutsertaan ini dikonfigurasi sebagai bagian dari panggilan ke UseLazyLoadingProxies
. Contohnya:
optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());
Akses ke entitas terlacak
Cari entitas yang dilacak berdasarkan kunci utama, alternatif, atau asing
Secara internal, EF mempertahankan struktur data untuk menemukan entitas yang dilacak berdasarkan kunci primer, alternatif, atau asing. Struktur data ini digunakan untuk perbaikan yang efisien antara entitas terkait ketika entitas baru dilacak atau hubungan berubah.
EF8 berisi API publik baru sehingga aplikasi sekarang dapat menggunakan struktur data ini untuk mencari entitas yang dilacak secara efisien. API ini diakses melalui LocalView<TEntity> jenis entitas. Misalnya, untuk mencari entitas terlacak dengan kunci utamanya:
var blogEntry = context.Blogs.Local.FindEntry(2)!;
Tip
Kode yang ditampilkan di sini berasal dari LookupByKeySample.cs.
Metode mengembalikan FindEntry
EntityEntry<TEntity> untuk entitas yang dilacak, atau null
jika tidak ada entitas dengan kunci yang diberikan yang sedang dilacak. Seperti semua metode pada LocalView
, database tidak pernah dikueri, bahkan jika entitas tidak ditemukan. Entri yang dikembalikan berisi entitas itu sendiri, serta informasi pelacakan. Contohnya:
Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key {blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");
Mencari entitas dengan apa pun selain kunci primer mengharuskan nama properti ditentukan. Misalnya, untuk mencari dengan kunci alternatif:
var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new Uri("https://www.bricelam.net/"))!;
Atau untuk mencari dengan kunci asing yang unik:
var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri), new Uri("https://www.bricelam.net/"))!;
Sejauh ini, pencarian selalu mengembalikan satu entri, atau null
. Namun, beberapa pencarian dapat mengembalikan lebih dari satu entri, seperti saat mencari oleh kunci asing yang tidak unik. Metode GetEntries
harus digunakan untuk pencarian ini. Contohnya:
var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);
Dalam semua kasus ini, nilai yang digunakan untuk pencarian adalah kunci primer, kunci alternatif, atau nilai kunci asing. EF menggunakan struktur data internalnya untuk pencarian ini. Namun, pencarian berdasarkan nilai juga dapat digunakan untuk nilai properti atau kombinasi properti apa pun. Misalnya, untuk menemukan semua postingan yang diarsipkan:
var archivedPostEntries = context.Posts.Local.GetEntries(nameof(Post.Archived), true);
Pencarian ini memerlukan pemindaian semua instans yang dilacak Post
, sehingga akan kurang efisien daripada pencarian utama. Namun, biasanya masih lebih cepat daripada kueri naif menggunakan ChangeTracker.Entries<TEntity>().
Akhirnya, dimungkinkan juga untuk melakukan pencarian terhadap kunci komposit, kombinasi lain dari beberapa properti, atau ketika jenis properti tidak diketahui pada waktu kompilasi. Contohnya:
var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new object[] { 4, "TagEF" });
Bangunan model
Kolom diskriminator memiliki panjang maksimal
Di EF8, kolom diskriminator string yang digunakan untuk pemetaan warisan TPH sekarang dikonfigurasi dengan panjang maksimum. Panjang ini dihitung sebagai angka Fibonacci terkecil yang mencakup semua nilai diskriminator yang ditentukan. Misalnya, pertimbangkan hierarki berikut:
public abstract class Document
{
public int Id { get; set; }
public string Title { get; set; }
}
public abstract class Book : Document
{
public string? Isbn { get; set; }
}
public class PaperbackEdition : Book
{
}
public class HardbackEdition : Book
{
}
public class Magazine : Document
{
public int IssueNumber { get; set; }
}
Dengan konvensi penggunaan nama kelas untuk nilai diskriminator, nilai yang mungkin di sini adalah "PaperbackEdition", "HardbackEdition", dan "Magazine", dan karenanya kolom diskriminator dikonfigurasi untuk panjang maksimum 21. Misalnya, saat menggunakan SQL Server:
CREATE TABLE [Documents] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Discriminator] nvarchar(21) NOT NULL,
[Isbn] nvarchar(max) NULL,
[IssueNumber] int NULL,
CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]),
Tip
Angka fibonacci digunakan untuk membatasi berapa kali migrasi dihasilkan untuk mengubah panjang kolom saat jenis baru ditambahkan ke hierarki.
DateOnly/TimeOnly didukung di SQL Server
Jenis DateOnly dan TimeOnly diperkenalkan di .NET 6 dan telah didukung untuk beberapa penyedia database (misalnya SQLite, MySQL, dan PostgreSQL) sejak pengenalan mereka. Untuk SQL Server, rilis terbaru paket Microsoft.Data.SqlClient yang menargetkan .NET 6 telah memungkinkan ErikEJ untuk menambahkan dukungan untuk jenis ini di tingkat ADO.NET. Ini pada gilirannya membuka jalan untuk dukungan di EF8 untuk DateOnly
dan TimeOnly
sebagai properti dalam jenis entitas.
Tip
DateOnly
dan TimeOnly
dapat digunakan dalam EF Core 6 dan 7 menggunakan paket komunitas ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly dari @ErikEJ.
Misalnya, pertimbangkan model EF berikut untuk sekolah-sekolah Inggris:
public class School
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly Founded { get; set; }
public List<Term> Terms { get; } = new();
public List<OpeningHours> OpeningHours { get; } = new();
}
public class Term
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly FirstDay { get; set; }
public DateOnly LastDay { get; set; }
public School School { get; set; } = null!;
}
[Owned]
public class OpeningHours
{
public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly? closesAt)
{
DayOfWeek = dayOfWeek;
OpensAt = opensAt;
ClosesAt = closesAt;
}
public DayOfWeek DayOfWeek { get; private set; }
public TimeOnly? OpensAt { get; set; }
public TimeOnly? ClosesAt { get; set; }
}
Tip
Kode yang ditampilkan di sini berasal dari DateOnlyTimeOnlySample.cs.
Catatan
Model ini hanya mewakili sekolah inggris dan menyimpan waktu sebagai waktu lokal (GMT). Menangani zona waktu yang berbeda akan mempersulit kode ini secara signifikan. Perhatikan bahwa penggunaan DateTimeOffset
tidak akan membantu di sini, karena waktu buka dan tutup memiliki offset yang berbeda tergantung apakah waktu musim panas aktif atau tidak.
Jenis entitas ini memetakan ke tabel berikut saat menggunakan SQL Server. Perhatikan bahwa properti memetakan DateOnly
ke date
kolom, dan properti memetakan TimeOnly
ke time
kolom.
CREATE TABLE [Schools] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Founded] date NOT NULL,
CONSTRAINT [PK_Schools] PRIMARY KEY ([Id]));
CREATE TABLE [OpeningHours] (
[SchoolId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[DayOfWeek] int NOT NULL,
[OpensAt] time NULL,
[ClosesAt] time NULL,
CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]),
CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
CREATE TABLE [Term] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[FirstDay] date NOT NULL,
[LastDay] date NOT NULL,
[SchoolId] int NOT NULL,
CONSTRAINT [PK_Term] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
Kueri menggunakan DateOnly
dan TimeOnly
bekerja dengan cara yang diharapkan. Misalnya, kueri LINQ berikut menemukan sekolah yang saat ini terbuka:
openSchools = await context.Schools
.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours.Any(
o => o.DayOfWeek == dayOfWeek
&& o.OpensAt < time && o.ClosesAt >= time))
.ToListAsync();
Kueri ini diterjemahkan ke SQL berikut, seperti yang ditunjukkan oleh ToQueryString:
DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '19:53:40.4798052';
SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id], [o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt]
FROM [Schools] AS [s]
LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND [t].[LastDay] >= @__today_0) AND EXISTS (
SELECT 1
FROM [OpeningHours] AS [o]
WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND [o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2)
ORDER BY [s].[Id], [o0].[SchoolId]
DateOnly
dan TimeOnly
juga dapat digunakan dalam kolom JSON. Misalnya, OpeningHours
dapat disimpan sebagai dokumen JSON, menghasilkan data yang terlihat seperti ini:
Kolom | Nilai |
---|---|
Id | 2 |
Nama | SMA Farr |
Didirikan | 1964-05-01 |
OpeningHours | [ |
Menggabungkan dua fitur dari EF8, kita sekarang dapat meminta jam buka dengan mengindeks ke dalam koleksi JSON. Contohnya:
openSchools = await context.Schools
.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours[(int)dayOfWeek].OpensAt < time
&& s.OpeningHours[(int)dayOfWeek].ClosesAt >= time)
.ToListAsync();
Kueri ini diterjemahkan ke SQL berikut, seperti yang ditunjukkan oleh ToQueryString:
DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '20:14:34.7795877';
SELECT [s].[Id], [s].[Founded], [s].[Name], [s].[OpeningHours]
FROM [Schools] AS [s]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0
AND [t].[LastDay] >= @__today_0)
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].OpensAt') AS time) < @__time_2
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].ClosesAt') AS time) >= @__time_2
Terakhir, pembaruan dan penghapusan dapat dilakukan dengan pelacakan dan SaveChanges, atau menggunakan ExecuteUpdate/ExecuteDelete. Contohnya:
await context.Schools
.Where(e => e.Terms.Any(t => t.LastDay.Year == 2022))
.SelectMany(e => e.Terms)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LastDay, t => t.LastDay.AddDays(1)));
Pembaruan ini diterjemahkan ke SQL berikut:
UPDATE [t0]
SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay])
FROM [Schools] AS [s]
INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) = 2022)
Reverse engineer Synapse dan Dynamics 365 TDS
Rekayasa terbalik EF8 (alias perancah dari database yang ada) sekarang mendukung Synapse Serverless SQL Pool dan Dynamics 365 database Titik Akhir TDS.
Peringatan
Sistem database ini memiliki perbedaan dari database SQL Server dan Azure SQL normal. Perbedaan ini berarti bahwa tidak semua fungsionalitas EF Core didukung saat menulis kueri terhadap atau melakukan operasi lain dengan sistem database ini.
Penyempurnaan terjemahan Matematika
Antarmuka matematika generik diperkenalkan di .NET 7. Jenis konkret seperti double
dan float
menerapkan antarmuka ini yang menambahkan API baru yang mencerminkan fungsionalitas Matematika dan MathF yang ada.
EF Core 8 menerjemahkan panggilan ke API matematika generik ini di LINQ menggunakan terjemahan SQL penyedia yang ada untuk Math
dan MathF
. Ini berarti Anda sekarang bebas memilih antara panggilan baik seperti Math.Sin
atau double.Sin
dalam kueri EF Anda.
Kami bekerja dengan tim .NET untuk menambahkan dua metode matematika generik baru di .NET 8 yang diimplementasikan pada double
dan float
. Ini juga diterjemahkan ke SQL di EF Core 8.
.NET | SQL |
---|---|
DegreesToRadians | RADIANS |
RadiansToDegrees | DEGREES |
Terakhir, kami bekerja dengan Eric Sink dalam proyek SQLitePCLRaw untuk mengaktifkan fungsi matematika SQLite dalam build pustaka SQLite asli. Ini termasuk pustaka asli yang Anda dapatkan secara default saat Anda menginstal penyedia EF Core SQLite. Ini memungkinkan beberapa terjemahan SQL baru di LINQ termasuk: Acos, Acosh, Asin, Asinh, Atan, Atan2, Atanh, Ceiling, Cos, Cosh, DegreesToRadians, Exp, Floor, Log, Log2, Log10, Pow, RadiansToDegrees, Sign, Sin, Sinh, Sqrt, Tan, Tanh, dan Truncate.
Memeriksa perubahan model yang tertunda
Kami telah menambahkan perintah baru dotnet ef
untuk memeriksa apakah ada perubahan model yang telah dilakukan sejak migrasi terakhir. Ini dapat berguna dalam skenario CI/CD untuk memastikan Anda atau rekan satu tim tidak lupa menambahkan migrasi.
dotnet ef migrations has-pending-model-changes
Anda juga dapat melakukan pemeriksaan ini secara terprogram dalam aplikasi atau pengujian Anda menggunakan metode baru dbContext.Database.HasPendingModelChanges()
.
Penyempurnaan perancah SQLite
SQLite hanya mendukung empat jenis data primitif--INTEGER, REAL, TEXT, dan BLOB. Sebelumnya, ini berarti bahwa ketika Anda merekayasa balik database SQLite untuk merancang model EF Core, jenis entitas yang dihasilkan hanya akan menyertakan properti jenis long
, , double
string
, dan byte[]
. Jenis .NET tambahan didukung oleh penyedia EF Core SQLite dengan mengonversi di antara mereka dan salah satu dari empat jenis SQLite primitif.
Di EF Core 8, kita sekarang menggunakan format data dan nama jenis kolom selain jenis SQLite untuk menentukan jenis .NET yang lebih sesuai untuk digunakan dalam model. Tabel berikut menunjukkan beberapa kasus di mana informasi tambahan mengarah ke jenis properti yang lebih baik dalam model.
Nama jenis kolom | Jenis .NET |
---|---|
BOOLEAN | |
SMALLINT | |
INT | |
BIGINT | long |
STRING |
Format data | Jenis .NET |
---|---|
'0.0' | |
'1970-01-01' | |
'1970-01-01 00:00:00' | |
'00:00:00' | |
'00000000-0000-0000-0000-000000000000' |
Nilai Sentinel dan default database
Database memungkinkan kolom dikonfigurasi untuk menghasilkan nilai default jika tidak ada nilai yang disediakan saat menyisipkan baris. Ini dapat diwakili dalam EF menggunakan HasDefaultValue
untuk konstanta:
b.Property(e => e.Status).HasDefaultValue("Hidden");
Atau HasDefaultValueSql
untuk klausul SQL arbitrer:
b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");
Tip
Kode yang ditunjukkan di bawah ini berasal dari DefaultConstraintSample.cs.
Agar EF dapat menggunakan ini, EF harus menentukan kapan dan kapan tidak mengirim nilai untuk kolom. Secara default, EF menggunakan default CLR sebagai sentinel untuk ini. Artinya, ketika nilai Status
atau LeaseDate
dalam contoh di atas adalah default CLR untuk jenis ini, maka EF menginterpretasikan bahwa berarti bahwa properti belum ditetapkan, sehingga tidak mengirim nilai ke database. Ini berfungsi dengan baik untuk jenis referensi--misalnya, jika string
properti Status
adalah null
, maka EF tidak mengirim null
ke database, melainkan tidak menyertakan nilai apa pun sehingga default database ("Hidden"
) digunakan. Demikian juga, untuk DateTime
properti LeaseDate
, EF tidak akan menyisipkan nilai default CLR dari 1/1/0001 12:00:00 AM
, tetapi akan menghilangkan nilai ini sehingga default database digunakan.
Namun, dalam beberapa kasus, nilai default CLR adalah nilai yang valid untuk disisipkan. EF8 menangani ini dengan memungkinkan nilai sentinel untuk kolom berubah. Misalnya, pertimbangkan kolom bilangan bulat yang dikonfigurasi dengan default database:
b.Property(e => e.Credits).HasDefaultValueSql(10);
Dalam hal ini, kami ingin entitas baru dimasukkan dengan jumlah kredit yang diberikan, kecuali ini tidak ditentukan, dalam hal ini 10 kredit ditetapkan. Namun, ini berarti bahwa memasukkan rekaman dengan kredit nol tidak dimungkinkan, karena nol adalah default CLR, dan karenanya akan menyebabkan EF tidak mengirim nilai. Di EF8, ini dapat diperbaiki dengan mengubah sentinel untuk properti dari nol menjadi -1
:
b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);
EF sekarang hanya akan menggunakan default database jika Credits
diatur ke -1
; nilai nol akan dimasukkan seperti jumlah lainnya.
Seringkali dapat berguna untuk mencerminkan ini dalam jenis entitas serta dalam konfigurasi EF. Contohnya:
public class Person
{
public int Id { get; set; }
public int Credits { get; set; } = -1;
}
Ini berarti bahwa nilai sentinel -1 diatur secara otomatis ketika instans dibuat, yang berarti bahwa properti dimulai dalam status "tidak ditetapkan".
Tip
Jika Anda ingin mengonfigurasi batasan default database untuk digunakan saat Migrations
membuat kolom, tetapi Anda ingin EF selalu menyisipkan nilai, maka konfigurasikan properti sebagai tidak dihasilkan. Contohnya,b.Property(e => e.Credits).HasDefaultValueSql(10).ValueGeneratedNever();
.
Default database untuk boolean
Properti Boolean menyajikan bentuk ekstrem dari masalah ini, karena default CLR (false
) adalah salah satu dari hanya dua nilai yang valid. Ini berarti bahwa bool
properti dengan batasan default database hanya akan memiliki nilai yang disisipkan jika nilai tersebut adalah true
. Ketika nilai default database adalah false
, maka ini berarti ketika nilai properti adalah false
, maka default database akan digunakan, yaitu false
. Jika tidak, jika nilai properti adalah true
, maka true
akan disisipkan. Jadi, ketika default database adalah false
, maka kolom database berakhir dengan nilai yang benar.
Di sisi lain, jika nilai default database adalah true
, ini berarti ketika nilai properti adalah false
, maka default database akan digunakan, yaitu true
! Dan ketika nilai properti adalah true
, maka true
akan disisipkan. Jadi, nilai dalam kolom akan selalu berakhir true
dalam database, terlepas dari apa nilai propertinya.
EF8 memperbaiki masalah ini dengan mengatur sentinel untuk properti bool ke nilai yang sama dengan nilai default database. Kedua kasus di atas kemudian mengakibatkan nilai yang benar dimasukkan, terlepas dari apakah default database adalah true
atau false
.
Tip
Saat perancah dari database yang ada, EF8 mengurai lalu menyertakan nilai default sederhana ke dalam HasDefaultValue
panggilan. (Sebelumnya, semua nilai default di-scaffolding sebagai panggilan buram HasDefaultValueSql
.) Ini berarti bahwa kolom bool yang tidak dapat diubah ke null dengan true
default database atau false
konstanta tidak lagi di-scaffolded sebagai nullable.
Default database untuk enum
Properti Enum dapat memiliki masalah bool
serupa dengan properti karena enum biasanya memiliki sekumpulan nilai yang valid yang sangat kecil, dan default CLR mungkin salah satu nilai ini. Misalnya, pertimbangkan jenis entitas dan enum ini:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; }
}
public enum Level
{
Beginner,
Intermediate,
Advanced,
Unspecified
}
Properti Level
kemudian dikonfigurasi dengan default database:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate);
Dengan konfigurasi ini, EF akan mengecualikan pengiriman nilai ke database saat diatur ke Level.Beginner
, dan sebagai gantinya Level.Intermediate
ditetapkan oleh database. Ini bukan apa yang dimaksudkan!
Masalah tidak akan terjadi jika enum telah ditentukan dengan nilai "tidak diketahui" atau "tidak ditentukan" menjadi default database:
public enum Level
{
Unspecified,
Beginner,
Intermediate,
Advanced
}
Namun, tidak selalu mungkin untuk mengubah enum yang ada, jadi di EF8, sentinel dapat kembali ditentukan. Misalnya, kembali ke enum asli:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate)
.HasSentinel(Level.Unspecified);
Sekarang Level.Beginner
akan disisipkan seperti biasa, dan default database hanya akan digunakan ketika nilai properti adalah Level.Unspecified
. Ini dapat kembali berguna untuk mencerminkan ini dalam jenis entitas itu sendiri. Contohnya:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; } = Level.Unspecified;
}
Menggunakan bidang backing nullable
Cara yang lebih umum untuk menangani masalah yang dijelaskan di atas adalah dengan membuat bidang dukungan nullable untuk properti yang tidak dapat diubah ke null. Misalnya, pertimbangkan jenis entitas berikut dengan bool
properti:
public class Account
{
public int Id { get; set; }
public bool IsActive { get; set; }
}
Properti dapat diberikan bidang dukungan nullable:
public class Account
{
public int Id { get; set; }
private bool? _isActive;
public bool IsActive
{
get => _isActive ?? false;
set => _isActive = value;
}
}
Bidang backing di sini akan tetap null
ada kecuali setter properti benar-benar dipanggil. Artinya, nilai bidang pencadangan adalah indikasi yang lebih baik tentang apakah properti telah ditetapkan atau tidak daripada default CLR properti. Ini berfungsi secara langsung dengan EF, karena EF akan menggunakan bidang pencadangan untuk membaca dan menulis properti secara default.
ExecuteUpdate dan ExecuteDelete yang Lebih Baik
Perintah SQL yang melakukan pembaruan dan penghapusan, seperti yang dihasilkan oleh ExecuteUpdate
metode dan ExecuteDelete
, harus menargetkan satu tabel database. Namun, di EF7, ExecuteUpdate
dan ExecuteDelete
tidak mendukung pembaruan yang mengakses beberapa jenis entitas bahkan ketika kueri pada akhirnya memengaruhi satu tabel. EF8 menghapus batasan ini. Misalnya, pertimbangkan Customer
jenis entitas dengan CustomerInfo
jenis yang dimiliki:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required CustomerInfo CustomerInfo { get; set; }
}
[Owned]
public class CustomerInfo
{
public string? Tag { get; set; }
}
Kedua jenis entitas ini memetakan ke Customers
tabel. Namun, pembaruan massal berikut gagal pada EF7 karena menggunakan kedua jenis entitas:
await context.Customers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(
s => s.SetProperty(b => b.CustomerInfo.Tag, "Tagged")
.SetProperty(b => b.Name, b => b.Name + "_Tagged"));
Di EF8, ini sekarang diterjemahkan ke SQL berikut saat menggunakan Azure SQL:
UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
[c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0
Demikian pula, instans yang Union
dikembalikan dari kueri dapat diperbarui selama pembaruan semua menargetkan tabel yang sama. Misalnya, kita dapat memperbarui apa pun Customer
dengan wilayah France
, dan pada saat yang sama, siapa pun Customer
yang telah mengunjungi toko dengan wilayah France
:
await context.CustomersWithStores
.Where(e => e.Region == "France")
.Union(context.Stores.Where(e => e.Region == "France").SelectMany(e => e.Customers))
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Tag, "The French Connection"));
Di EF8, kueri ini menghasilkan hal berikut saat menggunakan Azure SQL:
UPDATE [c]
SET [c].[Tag] = N'The French Connection'
FROM [CustomersWithStores] AS [c]
INNER JOIN (
SELECT [c0].[Id], [c0].[Name], [c0].[Region], [c0].[StoreId], [c0].[Tag]
FROM [CustomersWithStores] AS [c0]
WHERE [c0].[Region] = N'France'
UNION
SELECT [c1].[Id], [c1].[Name], [c1].[Region], [c1].[StoreId], [c1].[Tag]
FROM [Stores] AS [s]
INNER JOIN [CustomersWithStores] AS [c1] ON [s].[Id] = [c1].[StoreId]
WHERE [s].[Region] = N'France'
) AS [t] ON [c].[Id] = [t].[Id]
Sebagai contoh terakhir, di EF8, ExecuteUpdate
dapat digunakan untuk memperbarui entitas dalam hierarki TPT selama semua properti yang diperbarui dipetakan ke tabel yang sama. Misalnya, pertimbangkan jenis entitas ini yang dipetakan menggunakan TPT:
[Table("TptSpecialCustomers")]
public class SpecialCustomerTpt : CustomerTpt
{
public string? Note { get; set; }
}
[Table("TptCustomers")]
public class CustomerTpt
{
public int Id { get; set; }
public required string Name { get; set; }
}
Dengan EF8, Note
properti dapat diperbarui:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));
Name
Atau properti dapat diperbarui:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Namun, EF8 gagal mencoba memperbarui Name
properti dan Note
karena dipetakan ke tabel yang berbeda. Contohnya:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted")
.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Melemparkan pengecualian berikut:
The LINQ expression 'DbSet<SpecialCustomerTpt>()
.Where(s => s.Name == __name_0)
.ExecuteUpdate(s => s.SetProperty<string>(
propertyExpression: b => b.Note,
valueExpression: "Noted").SetProperty<string>(
propertyExpression: b => b.Name,
valueExpression: b => b.Name + " (Noted)"))' could not be translated. Additional information: Multiple 'SetProperty' invocations refer to different tables ('b => b.Note' and 'b => b.Name'). A single 'ExecuteUpdate' call can only update the columns of a single table. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
Penggunaan kueri yang IN
lebih baik
Contains
Ketika operator LINQ digunakan dengan subkueri, EF Core sekarang menghasilkan kueri yang lebih baik menggunakan SQL IN
alih-alih EXISTS
; selain menghasilkan SQL yang lebih mudah dibaca, dalam beberapa kasus ini dapat menghasilkan kueri yang lebih cepat secara dramatis. Misalnya, pertimbangkan kueri LINQ berikut:
var blogsWithPosts = await context.Blogs
.Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
.ToListAsync();
EF7 menghasilkan hal berikut untuk PostgreSQL:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE EXISTS (
SELECT 1
FROM "Posts" AS p
WHERE p."BlogId" = b."Id")
Karena subkueri mereferensikan tabel eksternal Blogs
(melalui b."Id"
), ini adalah subkueri berkorelasi, yang berarti bahwa Posts
subkueri harus dijalankan untuk setiap baris dalam Blogs
tabel. Di EF8, SQL berikut dihasilkan sebagai gantinya:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE b."Id" IN (
SELECT p."BlogId"
FROM "Posts" AS p
)
Karena subkueri tidak lagi mereferensikan , subkueri Blogs
dapat dievaluasi sekali, menghasilkan peningkatan performa besar-besaran pada sebagian besar sistem database. Namun, beberapa sistem database, terutama SQL Server, database dapat mengoptimalkan kueri pertama ke kueri kedua sehingga performanya sama.
Rowversion numerik untuk SQL Azure/SQL Server
Konkurensi optimis otomatis SQL Server ditangani menggunakan rowversion
kolom. adalah rowversion
nilai buram 8 byte yang diteruskan antara database, klien, dan server. Secara default, SqlClient mengekspos rowversion
jenis sebagai byte[]
, meskipun jenis referensi yang dapat diubah menjadi kecocokan yang buruk untuk rowversion
semantik. Di EF8, mudah untuk memetakan rowversion
kolom ke long
atau ulong
properti. Contohnya:
modelBuilder.Entity<Blog>()
.Property(e => e.RowVersion)
.IsRowVersion();
Eliminasi tanda kurung
Menghasilkan SQL yang dapat dibaca adalah tujuan penting untuk EF Core. Dalam EF8, SQL yang dihasilkan lebih dapat dibaca melalui penghapusan otomatis kurung yang tidak diperlukan. Misalnya, kueri LINQ berikut:
await ctx.Customers
.Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName != null)
.ToListAsync();
Diterjemahkan ke Azure SQL berikut saat menggunakan EF7:
SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ((([c].[Id] * 3) + 2) > 0 AND ([c].[FirstName] IS NOT NULL)) OR ([c].[LastName] IS NOT NULL)
Yang telah ditingkatkan menjadi berikut saat menggunakan EF8:
SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ([c].[Id] * 3 + 2 > 0 AND [c].[FirstName] IS NOT NULL) OR [c].[LastName] IS NOT NULL
Penolakan khusus untuk klausul RETURNING/OUTPUT
EF7 mengubah SQL pembaruan default yang akan digunakan RETURNING
/OUTPUT
untuk mengambil kembali kolom yang dihasilkan database. Beberapa kasus di mana diidentifikasi di mana ini tidak berfungsi, sehingga EF8 memperkenalkan penolakan eksplisit untuk perilaku ini.
Misalnya, untuk menolak OUTPUT
saat menggunakan penyedia SQL Server/Azure SQL:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlOutputClause(false));
Atau untuk menolak RETURNING
saat menggunakan penyedia SQLite:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlReturningClause(false));
Perubahan kecil lainnya
Selain peningkatan yang dijelaskan di atas, ada banyak perubahan yang lebih kecil yang dilakukan pada EF8. Drive ini termasuk:
- Kompatibilitas NativeAOT/pemangkasan untuk Microsoft.Data.Sqlite
- Izinkan Wilayah Pilihan Multi-wilayah atau Aplikasi di EF Core Cosmos
- SQLite: Tambahkan EF. Functions.Unhex
- Opsi Indeks SQL Server SortInTempDB dan DataCompression
- Perbolehkan koneksi 'batalkan pembukaan' antar konteks
- Menambahkan versi generik atribut EntityTypeConfiguration
- Kueri: tambahkan dukungan untuk memproyeksikan entitas JSON yang telah disusun
- Menghapus subkueri dan proyeksi yang tidak diperlukan saat menggunakan pengurutan tanpa batas/offset dalam operasi yang ditetapkan
- Izinkan pengumpulan DbContext dengan layanan singleton
- RestartSequenceOperation.StartValue opsional
- Izinkan UseSequence dan HiLo pada properti non-kunci
- Berikan informasi lebih lanjut ketika kesalahan 'Tidak ada DbContext ditemukan' yang dihasilkan
- Meneruskan perilaku pelacakan kueri ke pencegat materialisasi
- Menggunakan perbandingan kunci string yang tidak peka huruf besar/kecil di SQL Server
- Perbolehkan pengonversi nilai untuk mengubah DbType
- Mengatasi layanan aplikasi dalam layanan EF
- Izinkan transfer kepemilikan DbConnection dari aplikasi ke DbContext