Yang Baru di EF Core 9
EF Core 9 (EF9) adalah rilis berikutnya setelah EF Core 8 dan dijadwalkan rilis pada November 2024.
EF9 tersedia sebagai build harian yang berisi semua fitur EF9 terbaru dan tweak API. Sampel di sini menggunakan build harian ini.
Tip
Anda dapat menjalankan dan men-debug ke dalam sampel dengan mengunduh kode sampel dari GitHub. Setiap bagian di bawah ini menautkan ke kode sumber khusus untuk bagian tersebut.
EF9 menargetkan .NET 8, dan oleh karena itu dapat digunakan dengan .NET 8 (LTS) atau .NET 9.
Tip
Dokumen Apa yang Baru diperbarui untuk setiap pratinjau. Semua sampel disiapkan untuk menggunakan build harian EF9, yang biasanya memiliki beberapa minggu tambahan pekerjaan yang selesai dibandingkan dengan pratinjau terbaru. Kami sangat mendorong penggunaan build harian saat menguji fitur baru sehingga Anda tidak melakukan pengujian terhadap bit basi.
Azure Cosmos DB for NoSQL
EF 9.0 menghadirkan peningkatan besar pada penyedia EF Core untuk Azure Cosmos DB; bagian signifikan dari penyedia telah ditulis ulang untuk menyediakan fungsionalitas baru, memungkinkan bentuk kueri baru, dan menyelaraskan penyedia dengan praktik terbaik Azure Cosmos DB dengan lebih baik. Peningkatan tingkat tinggi utama tercantum di bawah ini; untuk daftar lengkapnya, lihat masalah epik ini.
Peringatan
Sebagai bagian dari peningkatan masuk ke penyedia, sejumlah perubahan yang melanggar berdampak tinggi harus dilakukan; jika Anda meningkatkan aplikasi yang ada, silakan baca bagian perubahan yang melanggar dengan hati-hati.
Penyempurnaan kueri dengan kunci partisi dan ID dokumen
Setiap dokumen yang disimpan dalam database Azure Cosmos DB memiliki ID sumber daya yang unik. Selain itu, setiap dokumen dapat berisi "kunci partisi" yang menentukan pemartisian logis data sehingga database dapat diskalakan secara efektif. Informasi selengkapnya tentang memilih kunci partisi dapat ditemukan di Pemartisian dan penskalaan horizontal di Azure Cosmos DB.
Di EF 9.0, penyedia Azure Cosmos DB secara signifikan lebih baik dalam mengidentifikasi perbandingan kunci partisi dalam kueri LINQ Anda, dan mengekstraknya untuk memastikan kueri Anda hanya dikirim ke partisi yang relevan; ini dapat sangat meningkatkan performa kueri Anda dan mengurangi biaya RU. Contohnya:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
Dalam kueri ini, penyedia secara otomatis mengenali perbandingan pada PartitionKey
; jika kita memeriksa log, kita akan melihat yang berikut:
Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")
Perhatikan bahwa WHERE
klausa tidak berisi PartitionKey
: bahwa perbandingan telah "diangkat" dan digunakan untuk menjalankan kueri hanya terhadap partisi yang relevan. Dalam versi sebelumnya, perbandingan dibiarkan dalam WHERE
klausul dalam banyak situasi, menyebabkan kueri dijalankan terhadap semua partisi dan menghasilkan peningkatan biaya dan mengurangi performa.
Selain itu, jika kueri Anda juga menyediakan nilai untuk properti ID dokumen, dan tidak menyertakan operasi kueri lainnya, penyedia dapat menerapkan pengoptimalan tambahan:
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
Log memperlihatkan hal berikut untuk kueri ini:
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
Di sini, tidak ada kueri SQL yang dikirimkan. Sebagai gantinya, penyedia melakukan titik bacaReadItem
, yang secara langsung mengambil dokumen yang diberikan kunci dan ID partisi. Ini adalah jenis bacaan yang paling efisien dan hemat biaya yang dapat Anda lakukan di Azure Cosmos DB; lihat dokumentasi Azure Cosmos DB untuk informasi selengkapnya tentang pembacaan titik.
Untuk mempelajari selengkapnya tentang mengkueri dengan kunci partisi dan baca titik, lihat halaman dokumentasi kueri.
Kunci partisi hierarkis
Tip
Kode yang ditampilkan di sini berasal dari HierarchicalPartitionKeysSample.cs.
Azure Cosmos DB awalnya mendukung satu kunci partisi, tetapi sejak itu memperluas kemampuan partisi untuk juga mendukung subpartisi melalui spesifikasi hingga tiga tingkat hierarki dalam kunci partisi. EF Core 9 menghadirkan dukungan penuh untuk kunci partisi hierarkis, memungkinkan Anda memanfaatkan performa dan penghematan biaya yang lebih baik yang terkait dengan fitur ini.
Kunci partisi ditentukan menggunakan API pembuatan model, biasanya di DbContext.OnModelCreating. Harus ada properti yang dipetakan dalam jenis entitas untuk setiap tingkat kunci partisi. Misalnya, pertimbangkan UserSession
jenis entitas:
public class UserSession
{
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
Kode berikut menentukan kunci partisi tiga tingkat menggunakan TenantId
properti , , UserId
dan SessionId
:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Tip
Definisi kunci partisi ini mengikuti contoh yang diberikan dalam Memilih kunci partisi hierarkis Anda dari dokumentasi Azure Cosmos DB.
Perhatikan bagaimana, dimulai dengan EF Core 9, properti dari jenis yang dipetakan dapat digunakan dalam kunci partisi. Untuk bool
jenis numerik dan, seperti int SessionId
properti , nilai digunakan langsung di kunci partisi. Jenis lain, seperti Guid UserId
properti , secara otomatis dikonversi ke string.
Saat mengkueri, EF secara otomatis mengekstrak nilai kunci partisi dari kueri dan menerapkannya ke API kueri Azure Cosmos DB untuk memastikan kueri dibatasi dengan tepat hingga jumlah partisi sekecil mungkin. Misalnya, pertimbangkan kueri LINQ berikut yang menyediakan ketiga nilai kunci partisi dalam hierarki:
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
Saat menjalankan kueri ini, EF Core akan mengekstrak nilai tenantId
parameter , , userId
dan sessionId
, dan meneruskannya ke API kueri Azure Cosmos DB sebagai nilai kunci partisi. Misalnya, lihat log dari menjalankan kueri di atas:
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
Perhatikan bahwa perbandingan kunci partisi telah dihapus dari WHERE
klausul, dan sebaliknya digunakan sebagai kunci partisi untuk eksekusi yang efisien: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
.
Untuk informasi selengkapnya, lihat dokumentasi tentang kueri dengan kunci partisi.
Kemampuan kueri LINQ yang ditingkatkan secara signifikan
Dalam EF 9.0, kemampuan terjemahan LINQ dari penyedia Azure Cosmos DB telah sangat diperluas, dan penyedia sekarang dapat menjalankan lebih banyak jenis kueri secara signifikan. Daftar lengkap penyempurnaan kueri terlalu panjang untuk dicantumkan, tetapi berikut adalah sorotan utama:
- Dukungan penuh untuk koleksi primitif EF, memungkinkan Anda melakukan kueri LINQ pada koleksi misalnya ints atau string. Lihat Apa yang baru dalam EF8: koleksi primitif untuk informasi selengkapnya.
- Dukungan untuk kueri arbitrer melalui koleksi non-primitif.
- Banyak operator LINQ tambahan sekarang didukung: pengindeksan ke dalam koleksi,
Length
/Count
,ElementAt
,Contains
, dan banyak lainnya. - Dukungan untuk operator agregat seperti
Count
danSum
. - Terjemahan fungsi tambahan (lihat dokumentasi pemetaan fungsi untuk daftar lengkap terjemahan yang didukung):
- Terjemahan untuk
DateTime
anggota komponen danDateTimeOffset
(DateTime.Year
,DateTimeOffset.Month
...). -
EF.Functions.IsDefined
danEF.Functions.CoalesceUndefined
sekarang memungkinkan berurusan denganundefined
nilai. -
string.Contains
,StartsWith
danEndsWith
sekarang mendukungStringComparison.OrdinalIgnoreCase
.
- Terjemahan untuk
Untuk daftar lengkap penyempurnaan kueri, lihat masalah ini:
Pemodelan yang disempurnakan selaras dengan standar Azure Cosmos DB dan JSON
EF 9.0 memetakan ke dokumen Azure Cosmos DB dengan cara yang lebih alami untuk database dokumen berbasis JSON, dan membantu beroperasi dengan sistem lain yang mengakses dokumen Anda. Meskipun ini memerlukan perubahan yang melanggar, API ada yang memungkinkan kembali ke perilaku pra-9.0 dalam semua kasus.
Properti yang disederhanakan id
tanpa diskriminator
Pertama, versi EF sebelumnya memasukkan nilai diskriminator ke dalam properti JSON id
, menghasilkan dokumen seperti berikut:
{
"id": "Blog|1099",
...
}
Ini dilakukan untuk memungkinkan dokumen dari berbagai jenis (misalnya Blog dan Posting) dan nilai kunci yang sama (1099) ada dalam partisi kontainer yang sama. Dimulai dengan EF 9.0, id
properti hanya berisi nilai kunci:
{
"id": 1099,
...
}
Ini adalah cara yang lebih alami untuk memetakan ke JSON, dan memudahkan alat dan sistem eksternal untuk berinteraksi dengan dokumen JSON yang dihasilkan EF; sistem eksternal tersebut umumnya tidak menyadari nilai diskriminator EF, yang secara default berasal dari jenis .NET.
Perhatikan bahwa ini adalah perubahan yang melanggar, karena EF tidak akan lagi dapat mengkueri dokumen yang ada dengan format lama id
. API telah diperkenalkan untuk kembali ke perilaku sebelumnya, lihat catatan perubahan yang melanggar dan dokumentasi untuk detail selengkapnya.
Properti diskriminator diganti namanya menjadi $type
Properti diskriminator default sebelumnya bernama Discriminator
. EF 9.0 mengubah default menjadi $type
:
{
"id": 1099,
"$type": "Blog",
...
}
Ini mengikuti standar yang muncul untuk polimorfisme JSON, memungkinkan interoperabilitas yang lebih baik dengan alat lain. Misalnya. System.Text.Json NET juga mendukung polimorfisme, menggunakan $type
sebagai nama properti diskriminator default (dokumen).
Perhatikan bahwa ini adalah perubahan yang melanggar, karena EF tidak akan lagi dapat mengkueri dokumen yang ada dengan nama properti diskriminator lama. Lihat catatan perubahan yang melanggar untuk detail tentang cara kembali ke penamaan sebelumnya.
Pencarian kesamaan vektor (pratinjau)
Azure Cosmos DB sekarang menawarkan dukungan pratinjau untuk pencarian kesamaan vektor. Pencarian vektor adalah bagian mendasar dari beberapa jenis aplikasi, termasuk AI, pencarian semantik, dan lainnya. Azure Cosmos DB memungkinkan Anda menyimpan vektor langsung di dokumen Anda bersama data lainnya, yang berarti Anda dapat melakukan semua kueri terhadap satu database. Ini dapat sangat menyederhanakan arsitektur Anda dan menghapus kebutuhan akan solusi database vektor khusus tambahan di tumpukan Anda. Untuk mempelajari selengkapnya tentang pencarian vektor Azure Cosmos DB, lihat dokumentasi.
Setelah kontainer Azure Cosmos DB Anda disiapkan dengan benar, menggunakan pencarian vektor melalui EF adalah masalah sederhana untuk menambahkan properti vektor dan mengonfigurasinya:
public class Blog
{
...
public float[] Vector { get; set; }
}
public class BloggingContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}
Setelah selesai, gunakan EF.Functions.VectorDistance()
fungsi dalam kueri LINQ untuk melakukan pencarian kesamaan vektor:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Untuk informasi selengkapnya, lihat dokumentasi tentang pencarian vektor.
Dukungan penomoran halaman
Penyedia Azure Cosmos DB sekarang memungkinkan penomoran halaman melalui hasil kueri melalui token kelanjutan, yang jauh lebih efisien dan hemat biaya daripada penggunaan Skip
tradisional dan Take
:
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
Operator baru ToPageAsync
mengembalikan CosmosPage
, yang mengekspos token kelanjutan yang dapat digunakan untuk melanjutkan kueri secara efisien di titik selanjutnya, mengambil 10 item berikutnya:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
Untuk informasi selengkapnya, lihat bagian dokumentasi tentang penomoran halaman.
FromSql untuk kueri SQL yang lebih aman
Penyedia Azure Cosmos DB telah mengizinkan kueri SQL melalui FromSqlRaw. Namun, API tersebut dapat rentan terhadap serangan injeksi SQL ketika data yang disediakan pengguna diinterpolasi atau digabungkan ke dalam SQL. Di EF 9.0, Anda sekarang dapat menggunakan metode baru FromSql
, yang selalu mengintegrasikan data berparameter sebagai parameter di luar SQL:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Untuk informasi selengkapnya, lihat bagian dokumentasi tentang penomoran halaman.
Akses berbasis peran
Azure Cosmos DB for NoSQL menyertakan sistem kontrol akses berbasis peran (RBAC) bawaan. Ini sekarang didukung oleh EF9 untuk semua operasi sarana data. Namun, Azure Cosmos DB SDK tidak mendukung RBAC untuk operasi bidang manajemen di Azure Cosmos DB. Gunakan AZURE Management API alih-alih EnsureCreatedAsync
dengan RBAC.
I/O sinkron sekarang diblokir secara default
Azure Cosmos DB for NoSQL tidak mendukung API sinkron (pemblokiran) dari kode aplikasi. Sebelumnya, EF menutupi ini dengan memblokir untuk Anda pada panggilan asinkron. Namun, ini keduanya mendorong penggunaan I/O sinkron, yang merupakan praktik buruk, dan dapat menyebabkan kebuntuan. Oleh karena itu, dimulai dengan EF 9, pengecualian dilemparkan ketika akses sinkron dicoba. Contohnya:
I/O sinkron masih dapat digunakan untuk saat ini dengan mengonfigurasi tingkat peringatan dengan tepat. Misalnya, pada OnConfiguring
jenis Anda DbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
Perhatikan, bagaimanapun, bahwa kami berencana untuk sepenuhnya menghapus dukungan sinkronisasi di EF 11, jadi mulailah memperbarui untuk menggunakan metode asinkron seperti ToListAsync
dan SaveChangesAsync
sesegera mungkin!
AOT dan kueri yang telah dikompilasi sebelumnya
Peringatan
NativeAOT dan prakomprekomilasi kueri adalah fitur yang sangat eksperimental, dan belum cocok untuk penggunaan produksi. Dukungan yang dijelaskan di bawah ini harus dilihat sebagai infrastruktur menuju fitur akhir, yang kemungkinan akan dirilis dengan EF 10. Kami mendorong Anda untuk bereksperimen dengan dukungan saat ini dan melaporkan pengalaman Anda, tetapi merekomendasikan untuk tidak menyebarkan aplikasi EF NativeAOT dalam produksi.
EF 9.0 menghadirkan dukungan eksperimental awal untuk .NET NativeAOT, yang memungkinkan penerbitan aplikasi yang dikompilasi sebelumnya yang memanfaatkan EF untuk mengakses database. Untuk mendukung kueri LINQ dalam mode NativeAOT, EF mengandalkan prakompilasi kueri: mekanisme ini secara statis mengidentifikasi kueri EF LINQ dan menghasilkan pencegat C#, yang berisi kode untuk menjalankan setiap kueri tertentu. Ini dapat secara signifikan mengurangi waktu startup aplikasi Anda, karena pengangkatan berat pemrosesan dan kompilasi kueri LINQ Anda ke SQL tidak lagi terjadi setiap kali aplikasi Anda dimulai. Sebagai gantinya, setiap pencegat kueri berisi SQL yang diselesaikan untuk kueri tersebut, serta kode yang dioptimalkan untuk mewujudkan hasil database sebagai objek .NET.
Misalnya, diberikan program dengan kueri EF berikut:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF akan menghasilkan pencegat C# ke dalam proyek Anda, yang akan mengambil alih eksekusi kueri. Alih-alih memproses kueri dan menerjemahkannya ke SQL setiap kali program dimulai, pencegat memiliki SQL yang disematkan langsung ke dalamnya (untuk SQL Server dalam hal ini), memungkinkan program Anda untuk memulai jauh lebih cepat:
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
Selain itu, pencegat yang sama berisi kode untuk mewujudkan objek .NET Anda dari hasil database:
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
Ini menggunakan fitur .NET baru lainnya - aksesor tidak aman, untuk menyuntikkan data dari database ke bidang privat objek Anda.
Jika Anda tertarik dengan NativeAOT dan ingin bereksperimen dengan fitur canggih, cobalah ini! Perlu diketahui bahwa fitur tersebut harus dianggap tidak stabil, dan saat ini memiliki banyak batasan; kami berharap untuk menstabilkannya dan membuatnya lebih cocok untuk penggunaan produksi di EF 10.
Lihat halaman dokumentasi NativeAOT untuk detail selengkapnya.
Terjemahan LINQ dan SQL
Seperti setiap rilis, EF9 menyertakan sejumlah besar peningkatan kemampuan kueri LINQ. Kueri baru dapat diterjemahkan, dan banyak terjemahan SQL untuk skenario yang didukung telah ditingkatkan, untuk performa dan keterbacaan yang lebih baik.
Jumlah peningkatan terlalu bagus untuk mencantumkan semuanya di sini. Di bawah ini, beberapa peningkatan yang lebih penting disorot; lihat masalah ini untuk daftar pekerjaan yang lebih lengkap yang dilakukan di 9.0.
Kami ingin memanggil Andrea Canciani (@ranma42) karena banyak kontribusi berkualitas tingginya untuk mengoptimalkan SQL yang dihasilkan oleh EF Core!
Jenis kompleks: Dukungan GroupBy dan ExecuteUpdate
GroupBy
Tip
Kode yang ditampilkan di sini berasal dari ComplexTypesSample.cs.
EF9 mendukung pengelompokan berdasarkan instans jenis kompleks. Contohnya:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF menerjemahkan ini sebagai pengelompokan oleh setiap anggota jenis kompleks, yang selaras dengan semantik jenis kompleks sebagai objek nilai. Misalnya, di Azure SQL:
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExecuteUpdate
Tip
Kode yang ditampilkan di sini berasal dari ExecuteUpdateSample.cs.
Demikian pula, di EF9 ExecuteUpdate
juga telah ditingkatkan untuk menerima properti jenis kompleks. Namun, setiap anggota jenis kompleks harus ditentukan secara eksplisit. Contohnya:
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));
Ini menghasilkan SQL yang memperbarui setiap kolom yang dipetakan ke jenis kompleks:
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
Sebelumnya, Anda harus mencantumkan properti yang berbeda dari jenis kompleks dalam panggilan Anda ExecuteUpdate
secara manual.
Memangkas elemen yang tidak diperlukan dari SQL
Sebelumnya, EF terkadang memproduksi SQL yang berisi elemen yang sebenarnya tidak diperlukan; dalam kebanyakan kasus, ini mungkin diperlukan pada tahap pemrosesan SQL sebelumnya, dan tertinggal. EF9 sekarang memangkas sebagian besar elemen tersebut, menghasilkan lebih ringkas dan, dalam beberapa kasus, SQL yang lebih efisien.
Pemangkasan tabel
Sebagai contoh pertama, SQL yang dihasilkan oleh EF terkadang berisi JOIN ke tabel yang sebenarnya tidak diperlukan dalam kueri. Pertimbangkan model berikut, yang menggunakan pemetaan pewarisan tabel per jenis (TPT):
public class Order
{
public int Id { get; set; }
...
public Customer Customer { get; set; }
}
public class DiscountedOrder : Order
{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...
public List<Order> Orders { get; set; }
}
public class BlogContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}
Jika kita kemudian menjalankan kueri berikut untuk mendapatkan semua Pelanggan dengan setidaknya satu Pesanan:
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 menghasilkan SQL berikut:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])
Perhatikan bahwa kueri berisi gabungan ke DiscountedOrders
tabel meskipun tidak ada kolom yang direferensikan di dalamnya. EF9 menghasilkan SQL yang diprakarsai tanpa gabungan:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Pemangkasan proyeksi
Demikian pula, mari kita periksa kueri berikut:
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
Pada EF8, kueri ini menghasilkan SQL berikut:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Perhatikan bahwa [o].[Id]
proyeksi tidak diperlukan dalam subkueri, karena ekspresi SELECT luar hanya menghitung baris. EF9 menghasilkan yang berikut ini sebagai gantinya:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... dan proyeksi kosong. Ini mungkin tidak tampak seperti banyak, tetapi dapat secara signifikan menyederhanakan SQL dalam beberapa kasus; Anda dipersilakan untuk menggulir beberapa perubahan SQL dalam pengujian untuk melihat efeknya.
Terjemahan yang melibatkan GREATEST/LEAST
Tip
Kode yang ditampilkan di sini berasal dari LeastGreatestSample.cs.
Beberapa terjemahan baru telah diperkenalkan yang menggunakan GREATEST
fungsi dan LEAST
SQL.
Penting
Fungsi GREATEST
dan LEAST
diperkenalkan ke database SQL Server/Azure SQL dalam versi 2022. Visual Studio 2022 menginstal SQL Server 2019 secara default. Sebaiknya instal SQL Server Developer Edition 2022 untuk mencoba terjemahan baru ini di EF9.
Misalnya, kueri yang menggunakan Math.Max
atau Math.Min
sekarang diterjemahkan untuk Azure SQL menggunakan GREATEST
dan LEAST
masing-masing. Contohnya:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:
SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >
Math.Min
dan Math.Max
juga dapat digunakan pada nilai koleksi primitif. Contohnya:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
Terakhir, RelationalDbFunctionsExtensions.Least
dan RelationalDbFunctionsExtensions.Greatest
dapat digunakan untuk langsung memanggil Least
fungsi atau Greatest
di SQL. Contohnya:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
Paksa atau cegah parameterisasi kueri
Tip
Kode yang ditampilkan di sini berasal dari QuerySample.cs.
Kecuali dalam beberapa kasus khusus, EF Core membuat parameter variabel yang digunakan dalam kueri LINQ, tetapi menyertakan konstanta dalam SQL yang dihasilkan. Misalnya, pertimbangkan metode kueri berikut:
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
Ini diterjemahkan ke SQL dan parameter berikut saat menggunakan Azure SQL:
Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
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 [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0
Perhatikan bahwa EF membuat konstanta di SQL untuk ".NET Blog" karena nilai ini tidak akan berubah dari kueri ke kueri. Menggunakan konstanta memungkinkan nilai ini diperiksa oleh mesin database saat membuat rencana kueri, berpotensi menghasilkan kueri yang lebih efisien.
Di sisi lain, nilai id
diparameterkan, karena kueri yang sama dapat dijalankan dengan banyak nilai yang berbeda untuk id
. Membuat konstanta dalam kasus ini akan mengakibatkan polusi cache kueri dengan banyak kueri yang hanya berbeda dalam id
nilai. Ini sangat buruk untuk performa keseluruhan database.
Secara umum, default ini tidak boleh diubah. Namun, EF Core 8.0.2 memperkenalkan EF.Constant
metode yang memaksa EF untuk menggunakan konstanta bahkan jika parameter akan digunakan secara default. Contohnya:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
Terjemahan sekarang berisi konstanta untuk id
nilai :
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
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 [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
Metode EF.Parameter
EF9 memperkenalkan EF.Parameter
metode untuk melakukan kebalikannya. Artinya, paksa EF untuk menggunakan parameter meskipun nilainya adalah konstanta dalam kode. Contohnya:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
Terjemahan sekarang berisi parameter untuk string ".NET Blog":
Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
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 [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
Koleksi primitif berparameter
EF8 mengubah cara beberapa kueri yang menggunakan koleksi primitif diterjemahkan. Saat kueri LINQ berisi koleksi primitif parameter, EF mengonversi kontennya menjadi JSON dan meneruskannya sebagai nilai parameter tunggal kueri:
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
Ini akan menghasilkan terjemahan berikut di SQL Server:
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
Ini memungkinkan kueri SQL yang sama untuk koleksi parameter yang berbeda (hanya nilai parameter yang berubah), tetapi dalam beberapa situasi dapat menyebabkan masalah performa karena database tidak dapat merencanakan kueri secara optimal. Metode EF.Constant
ini dapat digunakan untuk kembali ke terjemahan sebelumnya.
Kueri berikut ini menggunakan EF.Constant
efek tersebut:
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
SQL yang dihasilkan adalah sebagai berikut:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
Selain itu, EF9 memperkenalkan TranslateParameterizedCollectionsToConstants
opsi konteks yang dapat digunakan untuk mencegah parameterisasi pengumpulan primitif untuk semua kueri. Kami juga menambahkan pelengkap TranslateParameterizedCollectionsToParameters
yang memaksa parameterisasi koleksi primitif secara eksplisit (ini adalah perilaku default).
Tip
Metode ini EF.Parameter
mengambil alih opsi konteks. Jika Anda ingin mencegah parameterisasi koleksi primitif untuk sebagian besar kueri Anda (tetapi tidak semua), Anda dapat mengatur opsi TranslateParameterizedCollectionsToConstants
konteks dan menggunakan EF.Parameter
untuk kueri atau variabel individual yang ingin Anda parameterisasi.
Subkueri yang tidak terkait dengan inlined
Tip
Kode yang ditampilkan di sini berasal dari QuerySample.cs.
Di EF8, IQueryable yang direferensikan dalam kueri lain dapat dijalankan sebagai roundtrip database terpisah. Misalnya, pertimbangkan kueri LINQ berikut:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = await dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
Di EF8, kueri untuk dotnetPosts
dijalankan sebagai satu perjalanan pulang pergi, lalu hasil akhir dijalankan sebagai kueri kedua. Misalnya, di SQL Server:
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
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 [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
Dalam EF9, IQueryable
dalam dotnetPosts
inlined, menghasilkan satu perjalanan pulang pergi database:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
Fungsi agregat melalui subkueri dan agregat di SQL Server
EF9 meningkatkan terjemahan beberapa kueri kompleks menggunakan fungsi agregat yang terdiri dari subkueri atau fungsi agregat lainnya. Di bawah ini adalah contoh kueri tersebut:
var latestPostsAverageRatingByLanguage = await context.Blogs
.Select(x => new
{
x.Language,
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.LatestPostRating))
.ToListAsync();
Pertama, Select
komputasi LatestPostRating
untuk masing-masing Post
yang memerlukan subkueri saat menerjemahkan ke SQL. Nantinya dalam kueri, hasil ini diagregasi menggunakan Average
operasi. SQL yang dihasilkan terlihat sebagai berikut saat dijalankan di SQL Server:
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
Dalam versi sebelumnya EF Core akan menghasilkan SQL yang tidak valid untuk kueri serupa, mencoba menerapkan operasi agregat langsung melalui subkueri. Ini tidak diperbolehkan di SQL Server dan menghasilkan pengecualian. Prinsip yang sama berlaku untuk kueri menggunakan agregat melalui agregat lain:
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();
Catatan
Perubahan ini tidak memengaruhi Sqlite, yang mendukung agregat melalui subkueri (atau agregat lainnya) dan tidak mendukung LATERAL JOIN
(APPLY
). Di bawah ini adalah SQL untuk kueri pertama yang berjalan di Sqlite:
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
Kueri menggunakan Count != 0 dioptimalkan
Tip
Kode yang ditampilkan di sini berasal dari QuerySample.cs.
Di EF8, kueri LINQ berikut diterjemahkan untuk menggunakan fungsi SQL COUNT
:
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 sekarang menghasilkan terjemahan yang lebih efisien menggunakan EXISTS
:
SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")
Semantik C# untuk operasi perbandingan pada nilai nullable
Dalam perbandingan EF8 antara elemen nullable tidak dilakukan dengan benar untuk beberapa skenario. Di C#, jika satu atau kedua operan null, hasil operasi perbandingan adalah false; jika tidak, nilai operand yang terkandung dibandingkan. Di EF8 kami menggunakan untuk menerjemahkan perbandingan menggunakan semantik null database. Ini akan menghasilkan hasil yang berbeda dari kueri serupa menggunakan LINQ ke Objek. Selain itu, kami akan menghasilkan hasil yang berbeda ketika perbandingan dilakukan dalam filter vs proyeksi. Beberapa kueri juga akan menghasilkan hasil yang berbeda antara Sql Server dan Sqlite/Postgres.
Misalnya, kueri:
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
akan menghasilkan SQL berikut:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
yang memfilter entitas yang NullableIntOne
atau NullableIntTwo
diatur ke null.
Dalam EF9 kami memproduksi:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)
Perbandingan serupa dilakukan dalam proyeksi:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
menghasilkan SQL berikut:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]
yang mengembalikan false
untuk entitas yang NullableIntOne
atau NullableIntTwo
diatur ke null (bukan true
yang diharapkan dalam C#). Menjalankan skenario yang sama pada Sqlite yang dihasilkan:
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
yang menghasilkan Nullable object must have a value
pengecualian, karena terjemahan menghasilkan null
nilai untuk kasus di mana NullableIntOne
atau NullableIntTwo
null.
EF9 sekarang menangani skenario ini dengan benar, menghasilkan hasil yang konsisten dengan LINQ ke Objek dan di berbagai penyedia.
Peningkatan ini dikontribusikan oleh @ranma42. Terima kasih banyak!
Terjemahan Order
operator dan OrderDescending
LINQ
EF9 memungkinkan terjemahan operasi pemesanan yang disederhanakan LINQ (Order
dan OrderDescending
). Ini berfungsi mirip dengan OrderBy
/OrderByDescending
tetapi tidak memerlukan argumen. Sebaliknya, mereka menerapkan pengurutan default - untuk entitas, ini berarti pemesanan berdasarkan nilai kunci primer dan untuk jenis lain, mengurutkan berdasarkan nilai itu sendiri.
Di bawah ini adalah contoh kueri yang memanfaatkan operator pemesanan yang disederhanakan:
var orderOperation = await context.Blogs
.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();
Kueri ini setara dengan yang berikut ini:
var orderByEquivalent = await context.Blogs
.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
})
.ToListAsync();
dan menghasilkan SQL berikut:
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
Catatan
Order
dan OrderDescending
metode hanya didukung untuk kumpulan entitas, jenis kompleks atau skalar - mereka tidak akan bekerja pada proyeksi yang lebih kompleks, misalnya koleksi jenis anonim yang berisi beberapa properti.
Peningkatan ini dikontribusikan oleh alumnus Tim EF @bricelam. Terima kasih banyak!
Peningkatan terjemahan operator negasi logis (!)
EF9 membawa banyak pengoptimalan di sekitar SQL CASE/WHEN
, COALESCE
, negasi, dan berbagai konstruksi lainnya; sebagian besar disumbangkan oleh Andrea Canciani (@ranma42) - banyak terima kasih untuk semua ini! Di bawah ini, kami hanya akan merinci beberapa pengoptimalan ini sekeliling negasi logis.
Mari kita periksa kueri berikut:
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
Di EF8 kami akan menghasilkan SQL berikut:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
Dalam EF9 kami "mendorong" NOT
operasi ke dalam perbandingan:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Contoh lain, berlaku untuk SQL Server, adalah operasi bersyarkat yang dinegasikan.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
Dalam EF8 digunakan untuk menghasilkan blok berlapis CASE
:
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Di EF9 kami menghapus bersarang:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Di SQL Server, saat memproyeksikan properti bool yang dinegasikan:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 akan menghasilkan CASE
blok karena perbandingan tidak dapat muncul dalam proyeksi langsung dalam kueri SQL Server:
SELECT [p].[Title], CASE
WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]
Dalam EF9, terjemahan ini telah disederhanakan dan sekarang menggunakan bitwise NOT (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Dukungan yang lebih baik untuk Azure SQL dan Azure Synapse
EF9 memungkinkan lebih banyak fleksibilitas saat menentukan jenis SQL Server yang sedang ditargetkan. Alih-alih mengonfigurasi EF dengan UseSqlServer
, Anda sekarang dapat menentukan UseAzureSql
atau UseAzureSynapse
.
Ini memungkinkan EF menghasilkan SQL yang lebih baik saat menggunakan Azure SQL atau Azure Synapse. EF dapat memanfaatkan fitur khusus database (misalnya jenis khusus untuk JSON di Azure SQL), atau mengatasi batasannya (misalnya ESCAPE
klausa tidak tersedia saat menggunakan LIKE
di Azure Synapse).
Penyempurnaan kueri lainnya
- Dukungan kueri koleksi primitif yang diperkenalkan di EF8 telah diperluas untuk mendukung semua
ICollection<T>
jenis. Perhatikan bahwa ini hanya berlaku untuk koleksi parameter dan sebaris - koleksi primitif yang merupakan bagian dari entitas masih terbatas pada array, daftar, dan di EF9 juga array/daftar baca-saja. - Fungsi baru
ToHashSetAsync
untuk mengembalikan hasil kueri sebagaiHashSet
(#30033, dikontribusikan oleh @wertzui). -
TimeOnly.FromDateTime
danFromTimeSpan
sekarang diterjemahkan di SQL Server (#33678). -
ToString
lebih enum sekarang diterjemahkan (#33706, disumbangkan oleh @Danevandy99). -
string.Join
sekarang diterjemahkan ke CONCAT_WS dalam konteks non-agregat di SQL Server (#28899). -
EF.Functions.PatIndex
sekarang diterjemahkan ke fungsi SQL ServerPATINDEX
, yang mengembalikan posisi awal kemunculan pertama pola (#33702, @smnsht). -
Sum
danAverage
sekarang bekerja untuk desimal pada SQLite (#33721, dikontribusikan oleh @ranma42). - Perbaikan dan pengoptimalan ke
string.StartsWith
danEndsWith
(#31482). -
Convert.To*
metode sekarang dapat menerima argumen jenisobject
(#33891, dikontribusikan oleh @imangd). - Operasi Exclusive-Or (XOR) sekarang diterjemahkan pada SQL Server (#34071, dikontribusikan oleh @ranma42).
- Pengoptimalan sekeliling nullability untuk
COLLATE
operasi danAT TIME ZONE
(#34263, dikontribusikan oleh @ranma42). - Pengoptimalan untuk
DISTINCT
lebih dariIN
,EXISTS
dan mengatur operasi (#34381, dikontribusikan oleh @ranma42).
Di atas hanya beberapa peningkatan kueri yang lebih penting di EF9; lihat masalah ini untuk daftar yang lebih lengkap.
Migrasi
Perlindungan terhadap migrasi bersamaan
EF9 memperkenalkan mekanisme penguncian untuk melindungi dari beberapa eksekusi migrasi yang terjadi secara bersamaan, karena dapat meninggalkan database dalam keadaan rusak. Ini tidak terjadi ketika migrasi disebarkan ke lingkungan produksi menggunakan metode yang direkomendasikan, tetapi dapat terjadi jika migrasi diterapkan pada runtime menggunakan DbContext.Database.MigrateAsync()
metode . Sebaiknya terapkan migrasi saat penyebaran, bukan sebagai bagian dari startup aplikasi, tetapi itu dapat menghasilkan arsitektur aplikasi yang lebih rumit (misalnya saat menggunakan proyek .NET Aspire).
Catatan
Jika Anda menggunakan database Sqlite, lihat potensi masalah yang terkait dengan fitur ini.
Peringatkan ketika beberapa operasi migrasi tidak dapat dijalankan di dalam transaksi
Sebagian besar operasi yang dilakukan selama migrasi dilindungi oleh transaksi. Ini memastikan bahwa jika karena beberapa alasan migrasi gagal, database tidak berakhir dalam keadaan rusak. Namun, beberapa operasi tidak dibungkus dalam transaksi (misalnya operasi pada tabel yang dioptimalkan memori SQL Server, atau operasi pengubahan database seperti memodifikasi kolase database). Untuk menghindari kerusakan database jika terjadi kegagalan migrasi, disarankan agar operasi ini dilakukan dalam isolasi menggunakan migrasi terpisah. EF9 sekarang mendeteksi skenario ketika migrasi berisi beberapa operasi, salah satunya tidak dapat dibungkus dalam transaksi, dan mengeluarkan peringatan.
Penyemaian data yang ditingkatkan
EF9 memperkenalkan cara mudah untuk melakukan penyemaian data, yaitu mengisi database dengan data awal.
DbContextOptionsBuilder
sekarang berisi UseSeeding
metode dan UseAsyncSeeding
yang dijalankan ketika DbContext diinisialisasi (sebagai bagian EnsureCreatedAsync
dari ).
Catatan
Jika aplikasi telah berjalan sebelumnya, database mungkin sudah berisi data sampel (yang akan ditambahkan pada inisialisasi pertama konteks). Dengan demikian, UseSeeding
UseAsyncSeeding
harus memeriksa apakah data ada sebelum mencoba mengisi database. Ini dapat dicapai dengan mengeluarkan kueri EF sederhana.
Berikut adalah contoh bagaimana metode ini dapat digunakan:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
Informasi selengkapnya dapat ditemukan di sini.
Peningkatan migrasi lainnya
- Saat mengubah tabel yang ada menjadi tabel temporal SQL Server, ukuran kode migrasi telah berkurang secara signifikan.
Bangunan model
Model yang dikompilasi otomatis
Tip
Kode yang ditampilkan di sini berasal dari sampel NewInEFCore9.CompiledModels .
Model yang dikompilasi dapat meningkatkan waktu startup untuk aplikasi dengan model besar--yaitu jumlah jenis entitas dalam 100s atau 1000s. Dalam versi EF Core sebelumnya, model yang dikompilasi harus dibuat secara manual, menggunakan baris perintah. Contohnya:
dotnet ef dbcontext optimize
Setelah menjalankan perintah, baris seperti, .UseModel(MyCompiledModels.BlogsContextModel.Instance)
harus ditambahkan ke OnConfiguring
untuk memberi tahu EF Core untuk menggunakan model yang dikompilasi.
Dimulai dengan EF9, baris ini .UseModel
tidak lagi diperlukan ketika jenis aplikasi DbContext
berada dalam proyek/rakitan yang sama dengan model yang dikompilasi. Sebagai gantinya, model yang dikompilasi akan terdeteksi dan digunakan secara otomatis. Ini dapat dilihat dengan memiliki log EF setiap kali membangun model. Menjalankan aplikasi sederhana kemudian menunjukkan EF membangun model saat aplikasi dimulai:
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
Output dari berjalan dotnet ef dbcontext optimize
pada proyek model adalah:
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
Perhatikan bahwa output log menunjukkan bahwa model dibangun saat menjalankan perintah. Jika kita sekarang menjalankan aplikasi lagi, setelah membangun kembali tetapi tanpa membuat perubahan kode apa pun, maka outputnya adalah:
Starting application...
Model loaded with 2 entity types.
Perhatikan bahwa model tidak dibangun saat memulai aplikasi karena model yang dikompilasi terdeteksi dan digunakan secara otomatis.
Integrasi MSBuild
Dengan pendekatan di atas, model yang dikompilasi masih perlu diregenerasi secara manual ketika jenis atau DbContext
konfigurasi entitas diubah. Namun, EF9 dikirim dengan paket tugas MSBuild yang dapat secara otomatis memperbarui model yang dikompilasi ketika proyek model dibangun! Untuk memulai, instal paket NuGet Microsoft.EntityFrameworkCore.Tasks . Contohnya:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
Tip
Gunakan versi paket dalam perintah di atas yang cocok dengan versi EF Core yang Anda gunakan.
Kemudian aktifkan integrasi dengan mengatur EFOptimizeContext
properti dan EFScaffoldModelStage
dalam file Anda .csproj
. Contohnya:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>
Sekarang, jika kita membangun proyek, kita dapat melihat pengelogan pada waktu build yang menunjukkan bahwa model yang dikompilasi sedang dibangun:
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
Dan menjalankan aplikasi menunjukkan bahwa model yang dikompilasi telah terdeteksi dan karenanya model tidak dibangun lagi:
Starting application...
Model loaded with 2 entity types.
Sekarang, setiap kali model berubah, model yang dikompilasi akan secara otomatis dibangun kembali segera setelah proyek dibangun.
Untuk informasi selengkapnya, lihat Integrasi MSBuild.
Koleksi primitif baca-saja
Tip
Kode yang ditampilkan di sini berasal dari PrimitiveCollectionsSample.cs.
EF8 memperkenalkan dukungan untuk pemetaan array dan daftar jenis primitif yang dapat diubah. Ini telah diperluas di EF9 untuk menyertakan koleksi/daftar baca-saja. Secara khusus, EF9 mendukung koleksi yang dititik sebagai IReadOnlyList
, , IReadOnlyCollection
atau ReadOnlyCollection
. Misalnya, dalam kode berikut, DaysVisited
akan dipetakan oleh konvensi sebagai kumpulan tanggal primitif:
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
Koleksi baca-saja dapat didukung oleh koleksi normal yang dapat diubah jika diinginkan. Misalnya, dalam kode berikut, DaysVisited
dapat dipetakan sebagai kumpulan tanggal primitif, sambil tetap mengizinkan kode di kelas untuk memanipulasi daftar yang mendasar.
public class Pub
{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}
Koleksi ini kemudian dapat digunakan dalam kueri dengan cara normal. Misalnya, kueri LINQ ini:
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();
Yang diterjemahkan ke SQL berikut di SQLite:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
Tentukan faktor pengisian untuk kunci dan indeks
Tip
Kode yang ditampilkan di sini berasal dari ModelBuildingSample.cs.
EF9 mendukung spesifikasi faktor pengisian SQL Server saat menggunakan Migrasi Inti EF untuk membuat kunci dan indeks. Dari dokumen SQL Server, "Ketika indeks dibuat atau dibangun kembali, nilai faktor pengisian menentukan persentase ruang pada setiap halaman tingkat daun yang akan diisi dengan data, menyimpan sisanya di setiap halaman sebagai ruang kosong untuk pertumbuhan di masa mendatang."
Faktor pengisian dapat diatur pada kunci dan indeks utama dan alternatif tunggal atau komposit. Contohnya:
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
Saat diterapkan ke tabel yang ada, ini akan mengubah tabel ke faktor pengisian ke batasan:
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
Peningkatan ini dikontribusikan oleh @deano-hunter. Terima kasih banyak!
Membuat konvensi pembuatan model yang ada lebih dapat diperluas
Tip
Kode yang ditampilkan di sini berasal dari CustomConventionsSample.cs.
Konvensi pembangunan model publik untuk aplikasi diperkenalkan di EF7. Di EF9, kami telah mempermudah perluasan beberapa konvensi yang ada. Misalnya, kode untuk memetakan properti berdasarkan atribut di EF7 adalah ini:
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
Di EF9, ini dapat disederhanakan ke hal berikut:
public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
Memperbarui ApplyConfigurationsFromAssembly untuk memanggil konstruktor non-publik
Dalam versi EF Core sebelumnya, ApplyConfigurationsFromAssembly
metode ini hanya membuat jenis konfigurasi dengan konstruktor publik dan tanpa parameter. Di EF9, kami telah meningkatkan pesan kesalahan yang dihasilkan ketika ini gagal, dan juga mengaktifkan instansiasi oleh konstruktor non-publik. Ini berguna ketika menemukan konfigurasi bersama di kelas berlapis privat yang tidak boleh dibuat oleh kode aplikasi. Contohnya:
public class Country
{
public int Code { get; set; }
public required string Name { get; set; }
private class FooConfiguration : IEntityTypeConfiguration<Country>
{
private FooConfiguration()
{
}
public void Configure(EntityTypeBuilder<Country> builder)
{
builder.HasKey(e => e.Code);
}
}
}
Selain itu, beberapa orang berpikir pola ini adalah kelalaian karena menggabungkan jenis entitas ke konfigurasi. Orang lain berpikir itu sangat berguna karena menemukan konfigurasi bersama dengan jenis entitas. Jangan perdebatkan ini di sini. :-)
HierarkiId SQL Server
Tip
Kode yang ditampilkan di sini berasal dari HierarchyIdSample.cs.
Gula untuk pembuatan jalur HierarkiId
Dukungan kelas satu untuk jenis SQL Server HierarchyId
ditambahkan di EF8. Dalam EF9, metode gula telah ditambahkan untuk mempermudah pembuatan simpul anak baru dalam struktur pohon. Misalnya, kueri kode berikut untuk entitas yang sudah ada dengan HierarchyId
properti:
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
Properti ini HierarchyId
kemudian dapat digunakan untuk membuat simpul anak tanpa manipulasi string eksplisit. Contohnya:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
Jika daisy
memiliki HierarchyId
, /4/1/3/1/
maka, child1
akan mendapatkan HierarchyId
"/4/1/3/1/1/", dan child2
akan mendapatkan HierarchyId
"/4/1/3/1/2/".
Untuk membuat simpul antara kedua anak ini, sub-tingkat tambahan dapat digunakan. Contohnya:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
Ini membuat simpul dengan HierarchyId
, /4/1/3/1/1.5/
menempatkannya di antara child1
dan child2
.
Peningkatan ini dikontribusikan oleh @Rezakazemi890. Terima kasih banyak!
Alat
Lebih sedikit pembangunan ulang
Alat dotnet ef
baris perintah secara default membangun proyek Anda sebelum menjalankan alat. Ini karena tidak membangun kembali sebelum menjalankan alat adalah sumber umum kebingungan ketika hal-hal tidak berfungsi. Pengembang berpengalaman dapat menggunakan --no-build
opsi untuk menghindari build ini, yang mungkin lambat. Namun, bahkan opsi dapat --no-build
menyebabkan proyek dibangun kembali saat berikutnya dibangun di luar alat EF.
Kami percaya kontribusi komunitas dari @Suchiman telah memperbaiki ini. Namun, kami juga sadar bahwa tweak di sekitar perilaku MSBuild memiliki kecenderungan untuk memiliki konsekuensi yang tidak diinginkan, jadi kami meminta orang-orang seperti Anda untuk mencoba ini dan melaporkan kembali pengalaman negatif apa pun yang Anda miliki.