Novinky v EF Core 9
EF Core 9 (EF9) je příští verze po EF Core 8 a plánuje se vydat v listopadu 2024.
EF9 je k dispozici jako denní buildy , které obsahují všechny nejnovější funkce EF9 a vylepšení rozhraní API. Zde uvedené ukázky využívají tyto denní buildy.
Tip
Ukázky můžete spustit a ladit stažením ukázkového kódu z GitHubu. Každá část níže odkazuje na zdrojový kód specifický pro tento oddíl.
EF9 cílí na .NET 8, a proto se dá použít s .NET 8 (LTS) nebo .NET 9 Preview.
Tip
Dokumentace Co je nového se aktualizuje pro jednotlivé verze Preview. Všechny ukázky jsou nastavené tak, aby používaly denní buildy EF9, které mají v porovnání s nejnovější verzí Preview obvykle několik dalších týdnů dokončené práce. Důrazně doporučujeme používat denní buildy při testování nových funkcí, abyste neprovádí testování proti zastaralým bitům.
Azure Cosmos DB for NoSQL
EF 9.0 přináší významná vylepšení poskytovatele EF Core pro Azure Cosmos DB; významné části poskytovatele byly přepsány tak, aby poskytovaly nové funkce, umožňovaly nové formy dotazů a lépe odpovídaly osvědčeným postupům služby Azure Cosmos DB. Hlavní vylepšení vysoké úrovně jsou uvedena níže; Úplný seznam najdete v tomto námětovém problému.
Upozorňující
V rámci vylepšení prováděných s poskytovatelem bylo nutné provést řadu zásadních změn s velkým dopadem; Pokud upgradujete existující aplikaci, přečtěte si část zásadních změn pečlivě.
Vylepšení dotazování pomocí klíčů oddílů a ID dokumentů
Každý dokument uložený v databázi Azure Cosmos DB má jedinečné ID prostředku. Každý dokument navíc může obsahovat "klíč oddílu", který určuje logické dělení dat tak, aby bylo možné efektivně škálovat databázi. Další informace o výběru klíčů oddílů najdete v tématu Dělení a horizontální škálování ve službě Azure Cosmos DB.
V EF 9.0 je poskytovatel Služby Azure Cosmos DB výrazně lepší při identifikaci porovnání klíčů oddílů v dotazech LINQ a jejich extrahováním, aby se zajistilo, že se dotazy posílají jenom do příslušného oddílu; to může výrazně zlepšit výkon dotazů a snížit poplatky za RU. Příklad:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
V tomto dotazu zprostředkovatel automaticky rozpozná porovnání PartitionKey
. Pokud prozkoumáme protokoly, uvidíme následující:
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")
Všimněte si, že WHERE
klauzule neobsahuje PartitionKey
: toto porovnání bylo "zrušeno" a používá se k provedení dotazu pouze u příslušného oddílu. Vpředchozíchchcích jsme v předchozích verzích bylo v klauzuli ponecháno v WHERE
mnoha situacích, což způsobilo spuštění dotazu ve všech oddílech, což vedlo ke zvýšení nákladů a snížení výkonu.
Pokud váš dotaz také poskytuje hodnotu vlastnosti ID dokumentu a neobsahuje žádné další operace dotazu, může poskytovatel použít další optimalizaci:
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
Protokoly zobrazují pro tento dotaz následující:
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
Tady se vůbec neposílají žádné dotazy SQL. Místo toho poskytovatel provádí extrémně efektivní čtení bodů (ReadItem
API), které přímo načte dokument vzhledem k klíči oddílu a ID. Jedná se o nejúčinnější a nákladově nejefektivnější typ čtení, který můžete provádět ve službě Azure Cosmos DB; Další informace o čtení bodů najdete v dokumentaci ke službě Azure Cosmos DB.
Další informace o dotazování pomocí klíčů oddílů a čtení bodů najdete na stránce dokumentace k dotazování.
Hierarchické klíče oddílů
Tip
Zde uvedený kód pochází z HierarchicalPartitionKeysSample.cs.
Služba Azure Cosmos DB původně podporovala jeden klíč oddílu, ale od té doby rozšiřuje možnosti dělení, aby podporovala i dílčí dělení prostřednictvím specifikace až tří úrovní hierarchie v klíči oddílu. EF Core 9 přináší plnou podporu pro hierarchické klíče oddílů, což vám umožní využít lepší výkon a úsporu nákladů spojených s touto funkcí.
Klíče oddílů se zadají pomocí rozhraní API pro vytváření modelů, obvykle v DbContext.OnModelCreating. Pro každou úroveň klíče oddílu musí existovat mapovaná vlastnost typu entity. Představte si UserSession
například typ entity:
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!;
}
Následující kód určuje tříúrovňový klíč oddílu pomocí znaku TenantId
, UserId
a SessionId
vlastnosti:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Tip
Tato definice klíče oddílu se řídí příkladem v části Volba hierarchických klíčů oddílů z dokumentace ke službě Azure Cosmos DB.
Všimněte si, jak se od EF Core 9 dají v klíči oddílu použít vlastnosti libovolného mapovaného typu. U bool
číselných typů, jako je vlastnost int SessionId
, se hodnota používá přímo v klíči oddílu. Jiné typy, jako je vlastnost Guid UserId
, se automaticky převedou na řetězce.
Při dotazování ef automaticky extrahuje hodnoty klíče oddílu z dotazů a použije je na rozhraní API pro dotazy Azure Cosmos DB, aby se zajistilo, že dotazy budou odpovídajícím způsobem omezené na nejmenší možný počet oddílů. Představte si například následující dotaz LINQ, který poskytuje všechny tři hodnoty klíče oddílu v hierarchii:
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();
Při provádění tohoto dotazu EF Core extrahuje hodnoty tenantId
userId
a parametry a sessionId
předá je do rozhraní API pro dotazy Azure Cosmos DB jako hodnotu klíče oddílu. Podívejte se například na protokoly z výše uvedeného dotazu:
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"))
Všimněte si, že porovnání klíče oddílu WHERE
byly z klauzule odebrány a místo toho se používají jako klíč oddílu pro efektivní provádění: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
.
Další informace najdete v dokumentaci k dotazování pomocí klíčů oddílů.
Výrazně vylepšené možnosti dotazování LINQ
V EF 9.0 byly možnosti překladu LINQ poskytovatele Azure Cosmos DB značně rozšířeny a zprostředkovatel teď může provádět výrazně více typů dotazů. Úplný seznam vylepšení dotazů je pro seznam příliš dlouhý, ale tady jsou hlavní zvýraznění:
- Úplná podpora primitivních kolekcí EF, která umožňuje provádět dotazování LINQ na kolekce, jako jsou například inty nebo řetězce. Další informace najdete v tématu Co je nového v EF8: primitivní kolekce .
- Podpora libovolného dotazování na nemitivické kolekce.
- Nyní se podporuje spousta dalších operátorů LINQ: indexování do kolekcí,
Length
/Count
,ElementAt
,Contains
a mnoho dalších. - Podpora agregačních operátorů, jako
Count
jsou aSum
. - Další překlady funkcí (úplný seznam podporovaných překladů najdete v dokumentaci k mapování funkcí):
- Překlady členů
DateTime
komponentDateTimeOffset
(DateTime.Year
,DateTimeOffset.Month
...). EF.Functions.IsDefined
aEF.Functions.CoalesceUndefined
teď povolte práci sundefined
hodnotami.string.Contains
StartsWith
aEndsWith
nyní podporuStringComparison.OrdinalIgnoreCase
.
- Překlady členů
Úplný seznam vylepšení dotazů najdete v tomto problému:
Vylepšené modelování sladěné se standardy Azure Cosmos DB a JSON
EF 9.0 se mapuje na dokumenty Azure Cosmos DB přirozeněji pro databázi dokumentů založenou na formátu JSON a pomáhá spolupracovat s jinými systémy, které přistupují k dokumentům. I když to zahrnuje zásadní změny, rozhraní API existují, která umožňují vrátit se zpět k chování před 9.0 ve všech případech.
Zjednodušené id
vlastnosti bez diskriminátoru
Zaprvé, předchozí verze EF vložily diskriminující hodnotu do vlastnosti JSON id
a vytvořily například následující dokumenty:
{
"id": "Blog|1099",
...
}
To bylo provedeno tak, aby bylo možné dokumenty různých typů (např. Blog a Příspěvek) a stejnou hodnotu klíče (1099) existovat ve stejném oddílu kontejneru. Od EF 9.0 id
obsahuje vlastnost pouze hodnotu klíče:
{
"id": 1099,
...
}
Jedná se o přirozenější způsob mapování na JSON a usnadňuje externím nástrojům a systémům interakci s dokumenty JSON vygenerovanými efem; tyto externí systémy obecně neuvědomují diskriminující hodnoty EF, které jsou ve výchozím nastavení odvozené z typů .NET.
Všimněte si, že se jedná o zásadní změnu, protože EF už nebude moct dotazovat existující dokumenty se starým id
formátem. Rozhraní API bylo zavedeno, aby se vrátilo k předchozímu chování. Další podrobnosti najdete v poznámce k zásadní změně a v dokumentaci .
Diskriminující vlastnost přejmenována na $type
Výchozí diskriminující vlastnost byla dříve pojmenována Discriminator
. EF 9.0 změní výchozí hodnotu na $type
:
{
"id": 1099,
"$type": "Blog",
...
}
To se řídí nově vznikajícím standardem pro polymorfismus JSON, což umožňuje lepší interoperabilitu s jinými nástroji. Například. Net System.Text.Json také podporuje polymorfismus, který se používá $type
jako výchozí diskriminující název vlastnosti (docs).
Všimněte si, že se jedná o zásadní změnu, protože EF už nebude moct dotazovat existující dokumenty se starým názvem diskriminující vlastnosti. Podrobnosti o tom, jak se vrátit k předchozímu pojmenování, najdete v poznámce k zásadní změně.
Hledání vektorové podobnosti (Preview)
Azure Cosmos DB teď nabízí podporu náhledu vyhledávání vektorové podobnosti. Vektorové vyhledávání je základní součástí některých typů aplikací, včetně AI, sémantického vyhledávání a dalších. Azure Cosmos DB umožňuje ukládat vektory přímo do dokumentů společně se zbytkem dat, což znamená, že můžete provádět všechny dotazy na jednu databázi. To může výrazně zjednodušit architekturu a odstranit potřebu dalšího vyhrazeného řešení vektorové databáze ve vašem zásobníku. Další informace o vektorovém vyhledávání ve službě Azure Cosmos DB najdete v dokumentaci.
Jakmile je kontejner Azure Cosmos DB správně nastavený, je použití vektorového vyhledávání prostřednictvím EF jednoduchou otázkou přidání vektorové vlastnosti a její konfigurace:
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);
}
}
Jakmile to uděláte, použijte EF.Functions.VectorDistance()
funkci v dotazech LINQ k provádění vyhledávání podobnosti vektorů:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Další informace najdete v dokumentaci k hledání vektorů.
Podpora stránkování
Poskytovatel služby Azure Cosmos DB teď umožňuje stránkovat výsledky dotazů prostřednictvím tokenů pokračování, což je mnohem efektivnější a nákladově efektivnější než tradiční použití Skip
a 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
}
Nový ToPageAsync
operátor vrátí CosmosPage
token pro pokračování, který se dá použít k efektivnímu obnovení dotazu v pozdějším bodě a načtení dalších 10 položek:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
Další informace najdete v části dokumentace o stránkování.
FromSql pro bezpečnější dotazování SQL
Poskytovatel služby Azure Cosmos DB povolil dotazování SQL prostřednictvím FromSqlRaw. Toto rozhraní API ale může být náchylné k útokům prostřednictvím injektáže SQL, pokud jsou data poskytovaná uživatelem interpolovaná nebo zřetězená do SQL. V EF 9.0 teď můžete použít novou FromSql
metodu, která vždy integruje parametrizovaná data jako parametr mimo SQL:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Další informace najdete v části dokumentace o stránkování.
Přístup na základě rolí
Azure Cosmos DB for NoSQL obsahuje integrovaný systém řízení přístupu na základě role (RBAC). Ef9 to teď podporuje pro všechny operace roviny dat. Sada Azure Cosmos DB SDK ale nepodporuje řízení přístupu na základě role pro operace roviny správy ve službě Azure Cosmos DB. Místo RBAC používejte rozhraní API EnsureCreatedAsync
pro správu Azure.
Synchronní vstupně-výstupní operace jsou teď ve výchozím nastavení blokované.
Azure Cosmos DB for NoSQL nepodporuje synchronní (blokující) rozhraní API z kódu aplikace. Ef to dříve maskoval tím, že vás blokuje při asynchronních voláních. Obě tyto možnosti však podporují synchronní použití vstupně-výstupních operací, což je chybný postup a může způsobit zablokování. Od EF 9 se proto při pokusu o synchronní přístup vyvolá výjimka. Příklad:
Synchronní vstupně-výstupní operace je možné prozatím použít tak, že odpovídajícím způsobem nakonfigurujete úroveň upozornění. Například do OnConfiguring
typu DbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
Mějte však na paměti, že plánujeme plně odebrat podporu synchronizace v EF 11, takže začněte aktualizovat tak, aby používaly asynchronní metody, jako ToListAsync
a SaveChangesAsync
co nejdříve!
AOT a předem kompilované dotazy
Jak už jsme zmínili v úvodu, na pozadí probíhá spousta práce, která ef Core umožňuje spouštět bez kompilace JIT (just-in-time). Místo toho ef zkompiluje předem všechno potřebné ke spouštění dotazů v aplikaci. Tato kompilace AOT a související zpracování se stane součástí sestavování a publikování aplikace. V tuto chvíli ve verzi EF9 není k dispozici mnoho, které můžete používat vy, vývojář aplikací. Dokončené problémy v EF9, které podporují AOT a předem zkompilované dotazy, jsou však pro ty, kteří mají zájem, následující:
- Kompilovaný model: Místo reflexe pro vlastnosti a pole použijte statickou vazbu.
- Kompilovaný model: Generování lambda použitých při sledování změn
- Sledování změn a kanál aktualizace kompatibilní s AOT/oříznutím
- Přesměrování dotazu na předkompilovaný kód pomocí průsečíků
- Nastavení všech uzlů výrazů SQL jako quotable
- Generování kompilovaného modelu během sestavování
- Automatické zjišťování zkompilovaného modelu
- Nastavení ParameterExtractingExpressionVisitor umožňující extrahovat cesty k vyhodnocovaným fragmentům ve stromu
- Generování stromů výrazů v kompilovaných modelech (filtry dotazů, převaděče hodnot)
- Zajištění odolnosti LinqToCSharpSyntaxTranslator vůči více deklarací stejné proměnné ve vnořených oborech
- Optimalizace ParametrExtractingExpressionVisitor
Podívejte se sem, kde najdete příklady použití předem zkompilovaných dotazů, které jsou součástí prostředí.
Překlad LINQ a SQL
Stejně jako u každé verze EF9 zahrnuje velké množství vylepšení funkcí dotazování LINQ. Nové dotazy je možné přeložit a mnoho překladů SQL pro podporované scénáře bylo vylepšeno, aby se zlepšil výkon i čitelnost.
Počet vylepšení je příliš velký, aby je zde všechny vypsali. Níže jsou zvýrazněna některá z důležitějších vylepšení; podívejte se na tento problém , kde najdete kompletní výpis práce provedené ve verzi 9.0.
Chtěli bychom volat Andrea Canciani (@ranma42) pro své četné vysoce kvalitní příspěvky k optimalizaci SQL, který se generuje EF Core!
Komplexní typy: Podpora GroupBy a ExecuteUpdate
GroupBy
Tip
Zde uvedený kód pochází z ComplexTypesSample.cs.
EF9 podporuje seskupení podle komplexní instance typu. Příklad:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF to překládá jako seskupení podle každého člena komplexního typu, který odpovídá sémantice komplexních typů jako hodnotových objektů. Například v 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
Zde uvedený kód pochází z ExecuteUpdateSample.cs.
Podobně jsme v EF9 ExecuteUpdate
vylepšili také příjem složitých vlastností typu. Každý člen komplexního typu však musí být zadán explicitně. Příklad:
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));
Tím se vygeneruje SQL, který aktualizuje každý sloupec mapovaný na komplexní typ:
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'
Dříve jste museli ručně vypsat různé vlastnosti komplexního typu ve volání ExecuteUpdate
.
Vyřazení nepotřebných prvků z SQL
Ef někdy vytvořil SQL, který obsahoval prvky, které nebyly skutečně potřeba; ve většině případů byly pravděpodobně potřeba v dřívější fázi zpracování SQL a zůstaly za sebou. EF9 teď vyřezává většinu takových prvků, což vede k kompaktnějším a v některých případech efektivnějším SQL.
Vyřezávání tabulek
Jako první příklad obsahoval SQL vygenerovaný EF někdy joins do tabulek, které v dotazu ve skutečnosti nebyly potřeba. Představte si následující model, který používá mapování dědičnosti tabulek podle typu (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();
}
}
Pokud pak spustíme následující dotaz, abychom získali všechny zákazníky s alespoň jednou objednávkou:
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 vygeneroval následující SQL:
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])
Všimněte si, že dotaz obsahoval spojení s DiscountedOrders
tabulkou, i když na ni nebyly odkazovány žádné sloupce. EF9 vygeneruje vyřazený SQL bez spojení:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Projekce vyřezávání
Podívejme se na následující dotaz:
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
V EF8 tento dotaz vygeneroval následující SQL:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Všimněte si, že [o].[Id]
projekce není v poddotazu potřebná, protože vnější výraz SELECT jednoduše spočítá řádky. EF9 místo toho vygeneruje následující:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... a projekce je prázdná. To nemusí vypadat jako mnoho, ale v některých případech může výrazně zjednodušit SQL; Vítá vás procházení některých změn SQL v testech , abyste viděli efekt.
Překlady zahrnující GREATEST/LEAST
Tip
Zde uvedený kód pochází z LeastGreatestSample.cs.
Zavedlo se několik nových překladů, které používají GREATEST
funkce SQL.LEAST
Důležité
Funkce GREATEST
a LEAST
funkce byly zavedeny do databází SQL Serveru nebo Azure SQL ve verzi 2022. Visual Studio 2022 ve výchozím nastavení nainstaluje SQL Server 2019. Doporučujeme nainstalovat SQL Server Developer Edition 2022 vyzkoušet tyto nové překlady v EF9.
Například dotazy používající Math.Max
nebo Math.Min
se teď překládají pro Azure SQL pomocí GREATEST
a LEAST
v uvedeném pořadí. Příklad:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:
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
a Math.Max
lze je také použít pro hodnoty primitivní kolekce. Příklad:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:
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
RelationalDbFunctionsExtensions.Least
Nakonec a RelationalDbFunctionsExtensions.Greatest
lze ji použít k přímému Least
vyvolání nebo Greatest
funkce v SQL. Příklad:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:
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]
Vynucení nebo zabránění parametrizaci dotazů
Tip
Zde uvedený kód pochází z QuerySample.cs.
Kromě některých speciálních případů EF Core parametrizuje proměnné použité v dotazu LINQ, ale zahrnuje konstanty ve vygenerovaném SQL. Představte si například následující metodu dotazu:
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
To se při použití Azure SQL přeloží na následující sql a parametry:
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
Všimněte si, že EF vytvořil konstantu v SQL pro ".NET Blog", protože tato hodnota se nezmění z dotazu na dotaz. Použití konstanty umožňuje, aby tato hodnota byla zkoumána databázovým strojem při vytváření plánu dotazů, což může mít za následek efektivnější dotaz.
Na druhou stranu je hodnota id
parametrizována, protože stejný dotaz může být proveden s mnoha různými hodnotami pro id
. Vytvoření konstanty v tomto případě by vedlo k znečištění mezipaměti dotazů s velkým množstvím dotazů, které se liší pouze v id
hodnotách. To je velmi špatné pro celkový výkon databáze.
Obecně řečeno, tyto výchozí hodnoty by neměly být změněny. EF Core 8.0.2 však zavádí metodu EF.Constant
, která vynutí ef použití konstanty i v případě, že by byl parametr použit ve výchozím nastavení. Příklad:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
Překlad teď obsahuje konstantu id
pro hodnotu:
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
Metoda EF.Parameter
EF9 zavádí metodu EF.Parameter
, která provede opačnou akci. To znamená, že ef vynutit použití parametru i v případě, že hodnota je konstanta v kódu. Příklad:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
Překlad teď obsahuje parametr pro řetězec ".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
Parametrizované primitivní kolekce
EF8 změnil způsob překladu některých dotazů, které používají primitivní kolekce. Pokud dotaz LINQ obsahuje parametrizovanou primitivní kolekci, EF převede jeho obsah na JSON a předá ho jako jednu hodnotu parametru dotazu:
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
Výsledkem bude následující překlad na SQL Serveru:
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]
)
To umožňuje mít stejný dotaz SQL pro různé parametrizované kolekce (pouze změny hodnoty parametru), ale v některých situacích to může vést k problémům s výkonem, protože databáze nemůže optimálně naplánovat dotaz. Metodu EF.Constant
lze použít k návratu k předchozímu překladu.
V tomto smyslu se použije EF.Constant
následující dotaz:
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
Výsledný SQL je následující:
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)
EF9 navíc zavádí TranslateParameterizedCollectionsToConstants
možnost kontextu, která se dá použít k zabránění primitivnímu parametrizaci kolekce pro všechny dotazy. Přidali jsme také doplněk TranslateParameterizedCollectionsToParameters
, který vynutí parametrizaci primitivních kolekcí explicitně (toto je výchozí chování).
Tip
Metoda EF.Parameter
přepíše možnost kontextu. Pokud chcete zabránit parametrizaci primitivních kolekcí pro většinu dotazů (ale ne všechny), můžete nastavit kontextovou možnost TranslateParameterizedCollectionsToConstants
a použít EF.Parameter
pro dotazy nebo jednotlivé proměnné, které chcete parametrizovat.
Vložené nesouvisející poddotazy
Tip
Zde uvedený kód pochází z QuerySample.cs.
V EF8 je možné spustit příkaz IQueryable odkazovaný v jiném dotazu jako samostatnou odezvu databáze. Představte si například následující dotaz LINQ:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArray();
V EF8 se dotaz provede dotnetPosts
jako jedna odezva a konečné výsledky se spustí jako druhý dotaz. Například na SQL Serveru:
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
V EF9 IQueryable
je vložený inlinovaný dotnetPosts
, což vede k jedné databázi zaokrouhlení:
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
Agregace funkcí nad poddotazy a agregacemi na SQL Serveru
EF9 zlepšuje překlad některých složitých dotazů pomocí agregačních funkcí složených přes poddotazy nebo jiné agregační funkce. Níže je příklad takového dotazu:
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();
Select
Nejprve se vypočítá LatestPostRating
pro každou z nichPost
, která při převodu na SQL vyžaduje poddotaz. Později v dotazu se tyto výsledky agregují pomocí Average
operace. Výsledný SQL při spuštění na SQL Serveru vypadá následovně:
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]
V předchozích verzích EF Core vygenerovalo neplatné SQL pro podobné dotazy a pokusil se použít agregační operaci přímo u poddotazů. To není na SQL Serveru povolené a výsledkem je výjimka. Stejný princip se vztahuje na dotazy využívající agregaci nad jinou agregací:
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();
Poznámka:
Tato změna nemá vliv na Sqlite, který podporuje agregace nad poddotazy (nebo jiné agregace) a nepodporuje LATERAL JOIN
(APPLY
). Níže je sql pro první dotaz spuštěný v 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"
Dotazy využívající počet != 0 jsou optimalizované.
Tip
Zde uvedený kód pochází z QuerySample.cs.
V EF8 se následující dotaz LINQ přeložil tak, aby používal funkci SQL COUNT
:
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 teď vygeneruje efektivnější překlad pomocí 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")
Sémantika jazyka C# pro operace porovnání s hodnotami s možnou hodnotou null
V porovnání ef8 mezi prvky s možnou hodnotou null nebyly v některých scénářích provedeny správně. V jazyce C# je-li jeden nebo oba operandy null, výsledek operace porovnání je false; v opačném případě se porovnávají hodnoty obsažených operandů. V EF8 jsme použili k překladu porovnání pomocí sémantiky null databáze. Výsledkem by byly jiné výsledky než podobný dotaz pomocí LINQ to Objects. Kromě toho bychom při porovnání v porovnání v projekci filtru vytvořili jiné výsledky. Některé dotazy by také vytvářely různé výsledky mezi SQL Serverem a Sqlite nebo Postgresem.
Například dotaz:
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
by vygeneroval následující SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
které filtrují entity, jejichž NullableIntOne
nebo NullableIntTwo
jsou nastaveny na hodnotu null.
V EF9 vyrábíme:
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)
Podobné porovnání provedené v projekci:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
výsledkem je následující SQL:
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]
která vrací false
entity, jejichž NullableIntOne
nebo NullableIntTwo
jsou nastaveny na hodnotu null (nikoli true
očekávané v jazyce C#). Spuštění stejného scénáře ve vygenerovaném sqlite:
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
což vede k Nullable object must have a value
výjimce, protože překlad vytváří null
hodnotu pro případy, kdy NullableIntOne
nebo NullableIntTwo
jsou null.
EF9 teď správně zpracovává tyto scénáře a vytváří výsledky konzistentní s LINQ to Objects a mezi různými poskytovateli.
Toto vylepšení přispělo @ranma42. Mnohokrát děkujeme!
Order
Překlad operátorů a OrderDescending
operátorů LINQ
EF9 umožňuje překlad zjednodušených operací řazení LINQ (Order
a OrderDescending
). Tyto funkce fungují podobně jako OrderBy
/OrderByDescending
argument, ale nevyžadují argument. Místo toho použijí výchozí řazení – pro entity to znamená řazení na základě hodnot primárního klíče a pro jiné typy, řazení na základě samotných hodnot.
Níže je příklad dotazu, který využívá zjednodušené operátory řazení:
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();
Tento dotaz je ekvivalentní následujícímu:
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();
a vytvoří následující SQL:
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]
Poznámka:
Order
a OrderDescending
metody jsou podporovány pouze pro kolekce entit, komplexních typů nebo skalárů – nebudou fungovat na složitějších projekcích, například kolekcích anonymních typů obsahujících více vlastností.
Tímto vylepšením přispěl tým EF Team alumnus @bricelam. Mnohokrát děkujeme!
Vylepšený překlad logického operátoru negace (!)
EF9 přináší mnoho optimalizací kolem SQL CASE/WHEN
, , COALESCE
negace a různých dalších konstruktorů; většina z nich přispěla Andrea Canciani (@ranma42) - mnoho díky za všechny tyto! Níže si ukážeme několik těchto optimalizací kolem logické negace.
Pojďme se podívat na následující dotaz:
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
V EF8 bychom vytvořili následující SQL:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
V EF9 jsme operaci "push" NOT
do porovnání:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Dalším příkladem, který platí pro SQL Server, je negated podmíněná operace.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
V EF8 slouží k vytvoření vnořených CASE
bloků:
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]
V EF9 jsme odebrali vnoření:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
Při promítání negated bool vlastnosti na SQL Serveru:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 by vygeneroval CASE
blok, protože porovnání se v projekci nezobrazují přímo v dotazech SQL Serveru:
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]
V EF9 byl tento překlad zjednodušený a nyní používá bitové NE (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Lepší podpora Pro Azure SQL a Azure Synapse
EF9 umožňuje větší flexibilitu při zadávání typu SQL Serveru, který je cílem. Místo konfigurace EF s UseSqlServer
, můžete nyní zadat UseAzureSql
nebo UseAzureSynapse
.
Ef to umožňuje dosáhnout lepšího SQL při použití Azure SQL nebo Azure Synapse. EF může využívat funkce specifické pro databázi (např. vyhrazený typ PRO JSON v Azure SQL) nebo obejít jeho omezení (například ESCAPE
klauzule není dostupná při použití LIKE
v Azure Synapse).
Další vylepšení dotazů
- Podpora primitivních kolekcí pro dotazování zavedená v EF8 byla rozšířena tak, aby podporovala všechny
ICollection<T>
typy. Všimněte si, že platí pouze pro parametr a vložené kolekce – primitivní kolekce, které jsou součástí entit, jsou stále omezeny na pole, seznamy a v EF9 také pole/seznamy jen pro čtení. - Nové
ToHashSetAsync
funkce, které vrátí výsledky dotazu jako dotaz (HashSet
#30033, přispěl @wertzui). TimeOnly.FromDateTime
aFromTimeSpan
jsou nyní přeloženy na SQL Server (#33678).ToString
přes výčty je nyní přeložen (#33706, přispěl @Danevandy99).string.Join
nyní se překládá na CONCAT_WS v neagregačním kontextu na SQL Serveru (#28899).EF.Functions.PatIndex
nyní se přeloží na funkci SQL ServeruPATINDEX
, která vrátí počáteční pozici prvního výskytu vzoru (#33702, @smnsht).Sum
a nyní na SQLite (#33721, přispěl @ranma42) pracovatAverage
pro desetinné čárky.- Opravy a optimalizace pro
string.StartsWith
aEndsWith
(#31482). Convert.To*
metody nyní mohou přijímat argument typuobject
(#33891, přispěl @imangd).- Operace Exclusive-Or (XOR) je nyní přeložena na SQL Server (#34071, přispěl @ranma42).
- Optimalizace týkající se nullability pro
COLLATE
aAT TIME ZONE
operace (#34263, přispěla @ranma42). - Optimalizace pro
DISTINCT
víceEXISTS
IN
operací a nastavení operací (#34381, přispělo @ranma42).
Výše uvedené byly pouze některé důležitější vylepšení dotazů v EF9; podívejte se na tento problém s podrobnějším výpisem.
Migrace
Vylepšené dočasné migrace tabulek
Migrace vytvořená při změně existující tabulky na dočasnou tabulku byla pro EF9 zmenšena. Například v EF8 vytvoření jedné existující tabulky dočasnou tabulkou vznikne následující migrace:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "SiteUri",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Blogs",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("SqlServer:Identity", "1, 1")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
.OldAnnotation("SqlServer:Identity", "1, 1");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}
V EF9 teď stejná operace vede k mnohem menší migraci:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodEndColumn", true);
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}
Ochrana před souběžnými migracemi
EF9 zavádí mechanismus uzamčení, který chrání před několika prováděními migrace současně, protože by mohla opustit databázi v poškozeném stavu. K tomu nedojde, když se migrace nasadí do produkčního prostředí pomocí doporučených metod, ale může k tomu dojít, pokud se migrace použijí za běhu pomocí DbContext.Database.Migrate()
této metody. Doporučujeme použít migrace při nasazení, nikoli jako součást spuštění aplikace, ale to může vést ke složitějším architekturám aplikací (např. při použití projektů .NET Aspire).
Poznámka:
Pokud používáte databázi Sqlite, podívejte se na potenciální problémy spojené s touto funkcí.
Upozornění, když nejde spustit více operací migrace uvnitř transakce
Většina operací provedených během migrací je chráněna transakcí. Tím se zajistí, že pokud z nějakého důvodu migrace selže, databáze nebude ukončena v poškozeném stavu. Některé operace však nejsou zabalené do transakce (např. operace v tabulkách optimalizovaných pro paměť SQL Serveru nebo operace změny databáze, jako je úprava kolace databáze). Pokud se chcete vyhnout poškození databáze v případě selhání migrace, doporučujeme tyto operace provádět izolovaně pomocí samostatné migrace. EF9 teď detekuje scénář, kdy migrace obsahuje více operací, z nichž jedna nemůže být zabalena do transakce, a vydává upozornění.
Vylepšené počáteční vkládání dat
EF9 zavedl pohodlný způsob, jak provést počáteční data, který naplní databázi počátečními daty. DbContextOptionsBuilder
nyní obsahuje UseSeeding
a UseAsyncSeeding
metody, které se spustí při inicializaci DbContext (jako součást EnsureCreatedAsync
).
Poznámka:
Pokud byla aplikace spuštěná dříve, může databáze již obsahovat ukázková data (která by byla přidána při první inicializaci kontextu). Proto byste měli před pokusem o naplnění databáze zkontrolovat, UseSeeding
UseAsyncSeeding
jestli data existují. Toho lze dosáhnout vydáním jednoduchého dotazu EF.
Tady je příklad použití těchto metod:
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);
}
});
Další informace najdete tady.
Vytváření modelů
Automaticky kompilované modely
Tip
Zde uvedený kód pochází z ukázky NewInEFCore9.CompiledModels .
Zkompilované modely můžou zlepšit dobu spouštění aplikací s velkými modely – to je počet typů entit v 100 nebo 1000s. V předchozích verzích EF Core se kompilovaný model musel generovat ručně pomocí příkazového řádku. Příklad:
dotnet ef dbcontext optimize
Po spuštění příkazu se musí přidat řádek jako, aby OnConfiguring
ef Core řekl, .UseModel(MyCompiledModels.BlogsContextModel.Instance)
aby používal kompilovaný model.
Od EF9 už tento .UseModel
řádek není potřeba, pokud je typ aplikace DbContext
ve stejném projektu nebo sestavení jako zkompilovaný model. Místo toho se zkompilovaný model rozpozná a použije automaticky. To můžete vidět tak, že při vytváření modelu použijete protokol EF. Spuštění jednoduché aplikace pak při spuštění aplikace zobrazí sestavení modelu EF:
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
Výstup spuštění dotnet ef dbcontext optimize
v projektu modelu je:
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>
Všimněte si, že výstup protokolu označuje, že model byl vytvořen při spuštění příkazu. Pokud teď aplikaci spustíme znovu, po opětovném sestavení, ale bez provedení jakýchkoli změn kódu, bude výstup vypadat takto:
Starting application...
Model loaded with 2 entity types.
Všimněte si, že model nebyl při spuštění aplikace sestaven, protože byl zjištěn a automaticky použit zkompilovaný model.
Integrace nástroje MSBuild
S výše uvedeným přístupem se kompilovaný model musí při změně typů entit nebo DbContext
konfigurace ručně vygenerovat ručně. EF9 se však dodává s balíčkem MSBuild a cíle, který může automaticky aktualizovat zkompilovaný model při sestavení projektu modelu! Začněte instalací balíčku NuGet Microsoft.EntityFrameworkCore.Tasks . Příklad:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-preview.4.24205.3
Tip
Ve výše uvedeném příkazu použijte verzi balíčku, která odpovídá verzi EF Core, kterou používáte.
Potom integraci povolte nastavením EFOptimizeContext
vlastnosti na soubor .csproj
. Příklad:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>
Existují další, volitelné, MSBuild vlastnosti pro řízení způsobu sestavení modelu, ekvivalentní možnosti předávané na příkazovém řádku .dotnet ef dbcontext optimize
Tady jsou některé z nich:
Vlastnost MSBuild | Popis |
---|---|
EFOptimizeContext | Nastavte na true povolení automaticky kompilovaných modelů. |
DbContextName | DbContext třída, která se má použít. Pouze název třídy nebo plně kvalifikovaný s obory názvů. Pokud tuto možnost vynecháte, EF Core najde třídu kontextu. Pokud existuje více tříd kontextu, je tato možnost povinná. |
EFStartupProject | Relativní cesta k projektu po spuštění Výchozí hodnota je aktuální složka. |
EFTargetNamespace | Obor názvů, který se má použít pro všechny generované třídy. Ve výchozím nastavení se vygeneruje z kořenového oboru názvů a výstupního adresáře plus CompiledModels. |
V našem příkladu musíme zadat spouštěcí projekt:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>
Když teď projekt sestavíme, uvidíme protokolování v době sestavení, což znamená, že se sestavuje kompilovaný model:
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 --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.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
A spuštění aplikace ukazuje, že byl zjištěn zkompilovaný model, a proto model není znovu sestaven:
Starting application...
Model loaded with 2 entity types.
Když se teď model změní, zkompilovaný model se po sestavení projektu automaticky znovu sestaví.
Poznámka:
Pracujeme na některých problémech s výkonem provedených ve zkompilovaném modelu EF8 a EF9. Další informace najdete v tématu Problém 33483# .
Primitivní kolekce jen pro čtení
Tip
Zde uvedený kód pochází z PrimitiveCollectionsSample.cs.
EF8 zavedla podporu mapování polí a proměnlivých seznamů primitivních typů. Tato možnost byla v EF9 rozšířena tak, aby zahrnovala kolekce/seznamy jen pro čtení. Ef9 konkrétně podporuje kolekce, které jsou zadány jako IReadOnlyList
, IReadOnlyCollection
nebo ReadOnlyCollection
. Například v následujícím kódu DaysVisited
se konvence mapuje jako primitivní kolekce kalendářních dat:
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
Kolekci jen pro čtení lze v případě potřeby zálohovat normální a proměnlivou kolekcí. Například v následujícím kódu DaysVisited
lze mapovat jako primitivní kolekci kalendářních dat, zatímco stále umožňuje kódu ve třídě manipulovat s podkladovým seznamem.
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;
}
Tyto kolekce se pak dají použít v dotazech běžným způsobem. Například tento dotaz LINQ:
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();
To se přeloží na následující SQL na 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"
Určení faktoru fill-factor pro klíče a indexy
Tip
Zde uvedený kód pochází z ModelBuildingSample.cs.
EF9 podporuje specifikaci výplňového faktoru SQL Serveru při použití migrací EF Core k vytváření klíčů a indexů. Z dokumentace k SQL Serveru: Při vytvoření nebo opětovném vytvoření indexu určuje hodnota faktoru výplně procento mezery na každé stránce na úrovni listu, která se mají vyplnit daty, a zbytek na každé stránce si zarezervuje jako volné místo pro budoucí růst."
Výplňový faktor lze nastavit pro jeden nebo složený primární a alternativní klíč a indexy. Příklad:
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);
Při použití u existujících tabulek se tím změní tabulky na výplň na omezení:
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);
Tímto vylepšením přispěl @deano-hunter. Mnohokrát děkujeme!
Větší rozšiřitelnost stávajících konvencí vytváření modelů
Tip
Zde uvedený kód pochází z CustomConventionsSample.cs.
Zásady vytváření veřejných modelů pro aplikace byly zavedeny v EF7. V EF9 jsme usnadnili rozšíření některých stávajících konvencí. Kód pro mapování vlastností podle atributu v EF7 je například tento:
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;
}
}
}
}
V EF9 je to možné zjednodušit takto:
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;
}
}
Aktualizace ApplyConfigurationsFromAssembly pro volání neveřejných konstruktorů
Vpředchozíchch ApplyConfigurationsFromAssembly
V EF9 jsme vylepšili chybové zprávy vygenerované v případě selhání a také povolili vytváření instancí nepřístupnou konstruktorem. To je užitečné při spolulokaci konfigurace v privátní vnořené třídě, která by nikdy neměla být vytvořena instancí kódu aplikace. Příklad:
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);
}
}
}
Kromě toho si někteří lidé myslí, že tento vzor je obominace, protože spojuje typ entity s konfigurací. Jiní lidé si myslí, že je velmi užitečné, protože společně vyhledá konfiguraci s typem entity. Nediskutujme o tom tady. :-)
SQL Server HierarchyId
Tip
Zde uvedený kód pochází z HierarchyIdSample.cs.
Generování cesty Sugar for HierarchyId
V EF8 byla přidána podpora první třídy pro typ SQL ServeruHierarchyId
. V EF9 byla přidána metoda cukru, která usnadňuje vytváření nových podřízených uzlů ve stromové struktuře. Například následující kód se dotazuje na existující entitu HierarchyId
s vlastností:
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
Tuto HierarchyId
vlastnost lze pak použít k vytvoření podřízených uzlů bez explicitní manipulace s řetězci. Příklad:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
Pokud daisy
má hodnotu HierarchyId
/4/1/3/1/
, child1
získáte HierarchyId
hodnotu /4/1/3/1/1/" a child2
získá HierarchyId
hodnotu /4/1/3/1/2/.
K vytvoření uzlu mezi těmito dvěma podřízenými objekty je možné použít další dílčí úroveň. Příklad:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
Tím se vytvoří uzel s hodnotou HierarchyId
, která ho /4/1/3/1/1.5/
umístí mezi child1
a child2
.
Tímto vylepšením přispěl @Rezakazemi890. Mnohokrát děkujeme!
Nástroje
Méně opětovného sestavení
Nástroj dotnet ef
příkazového řádku ve výchozím nastavení sestaví projekt před spuštěním nástroje. Důvodem je to, že před spuštěním nástroje není opětovné sestavení, což je běžný zdroj nejasností, když věci nefungují. Zkušení vývojáři můžou použít --no-build
možnost vyhnout se tomuto sestavení, což může být pomalé. I tato --no-build
možnost ale může způsobit opětovné sestavení projektu při příštím sestavení mimo nástroje EF.
Věříme, že příspěvek komunity od @Suchiman to vyřešil. Uvědomujeme si ale také, že vylepšení chování nástroje MSBuild mají tendenci mít nezamýšlené důsledky, takže žádáme lidi, jako byste to vyzkoušeli, a hlásíme zpět všechny negativní zkušenosti, které máte.