Sdílet prostřednictvím


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.

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, UserIda 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 tenantIduserIda 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, Containsa mnoho dalších.
  • Podpora agregačních operátorů, jako Count jsou a Sum.
  • Další překlady funkcí (úplný seznam podporovaných překladů najdete v dokumentaci k mapování funkcí):
    • Překlady členů DateTime komponent DateTimeOffset (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined a EF.Functions.CoalesceUndefined teď povolte práci s undefined hodnotami.
    • string.ContainsStartsWith a EndsWith nyní podporu StringComparison.OrdinalIgnoreCase.

Ú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í CosmosPagetoken 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

Upozorňující

Nativní AOOT a předkompilace dotazů jsou vysoce experimentální funkce a ještě nejsou vhodné pro produkční použití. Níže popsaná podpora by se měla považovat za infrastrukturu pro konečnou funkci, která bude pravděpodobně vydána s EF 10. Doporučujeme, abyste experimentovali s aktuální podporou a sestavami vašich prostředí, ale doporučujeme nasadit aplikace EF NativeAOT v produkčním prostředí.

EF 9.0 přináší počáteční experimentální podporu pro .NET NativeAOT, což umožňuje publikování předem zkompilovaných aplikací, které využívají EF pro přístup k databázím. Pokud chcete podporovat dotazy LINQ v režimu NativeAOT, EF spoléhá na předkompilace dotazů: tento mechanismus staticky identifikuje dotazy EF LINQ a generuje průsečíky jazyka C#, které obsahují kód ke spuštění každého konkrétního dotazu. To může výrazně snížit čas spuštění vaší aplikace, protože náročné zpracování a kompilace dotazů LINQ do SQL se už neděje při každém spuštění aplikace. Místo toho zachytávání každého dotazu obsahuje finalizovaný JAZYK SQL pro tento dotaz a také optimalizovaný kód pro materializaci výsledků databáze jako objektů .NET.

Například při zadání programu s následujícím dotazem EF:

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

EF vygeneruje do projektu zachycovač C#, který převezme provádění dotazu. Místo zpracování dotazu a jeho překladu do SQL při každém spuštění programu má průsečík vložený SQL přímo do něj (v tomto případě SQL Server), což umožňuje, aby se program spustil mnohem rychleji:

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

Kromě toho stejný zachytávání obsahuje kód pro materializaci objektu .NET z výsledků databáze:

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

K vložení dat z databáze do privátních polí objektu se používá další nová funkce .NET – nebezpečné přístupové objekty.

Pokud vás zajímá NativeAOT a chcete experimentovat s špičkovými funkcemi, vyzkoušejte to! Mějte na paměti, že funkce by měla být považována za nestabilní a v současné době má mnoho omezení; očekáváme, že ho stabilizujeme a bude vhodnější pro produkční využití v EF 10.

Další podrobnosti najdete na stránce dokumentace NativeAOT.

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, , COALESCEnegace 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 a FromTimeSpan jsou nyní přeloženy na SQL Server (#33678).
  • ToStringpř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 Serveru PATINDEX , která vrátí počáteční pozici prvního výskytu vzoru (#33702, @smnsht).
  • Suma nyní na SQLite (#33721, přispěl @ranma42) pracovat Average pro desetinné čárky.
  • Opravy a optimalizace pro string.StartsWith a EndsWith (#31482).
  • Convert.To*metody nyní mohou přijímat argument typu object (#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 a AT TIME ZONE operace (#34263, přispěla @ranma42).
  • Optimalizace pro DISTINCT více EXISTS INoperací 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

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 spouš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.

Další vylepšení migrace

  • Při změně existující tabulky na dočasnou tabulku SQL Serveru se velikost kódu migrace výrazně snížila.

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 úloh MSBuild, který může automaticky aktualizovat kompilovaný 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

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 a EFScaffoldModelStage vlastností v .csproj souboru. Příklad:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</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
  --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í.

Další informace naleznete v tématu INTEGRACE NÁSTROJE MSBuild.

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, IReadOnlyCollectionnebo 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.