Novinky v EF Core 8
EF Core 8.0 (EF8) byl vydán v listopadu 2023.
Tip
Ukázky můžete spustit a ladit stažením ukázkového kódu z GitHubu. Každý oddíl odkazuje na zdrojový kód specifický pro tento oddíl.
EF8 vyžaduje sestavení sady .NET 8 SDK a vyžaduje spuštění modulu runtime .NET 8. EF8 se nespustí ve starších verzích .NET a nespustí se v rozhraní .NET Framework.
Objekty hodnot využívající komplexní typy
Objekty uložené do databáze je možné rozdělit do tří širokých kategorií:
- Objekty, které jsou nestrukturované a obsahují jednu hodnotu. Například ,
int
,Guid
string
,IPAddress
. Jedná se o (poněkud volně) označované jako "primitivní typy". - Objekty strukturované tak, aby držely více hodnot a kde je identita objektu definována hodnotou klíče. Například ,
Blog
,Post
Customer
. Tyto typy se nazývají "typy entit". - Objekty strukturované tak, aby držely více hodnot, ale objekt nemá žádnou klíč definující identitu. Například ,
Address
Coordinate
.
Před EF8 nebyl žádný dobrý způsob, jak mapovat třetí typ objektu. Vlastněné typy lze použít, ale vzhledem k tomu, že vlastněné typy jsou ve skutečnosti typy entit, mají sémantiku založenou na hodnotě klíče, i když je tato hodnota klíče skrytá.
EF8 teď podporuje "Komplexní typy" pro pokrytí tohoto třetího typu objektu. Objekty komplexního typu:
- Nejsou identifikovány nebo sledovány podle hodnoty klíče.
- Musí být definován jako součást typu entity. (Jinými slovy, nemůžete mít
DbSet
komplexní typ.) - Může to být typy hodnot .NET nebo odkazové typy.
- Instance mohou být sdíleny více vlastnostmi.
Jednoduchý příklad
Představte si například typ Address
:
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Address
se pak použije na třech místech v jednoduchém modelu zákazníka/objednávky:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Address Address { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Pojďme vytvořit a uložit zákazníka s jejich adresou:
var customer = new Customer
{
Name = "Willow",
Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" }
};
context.Add(customer);
await context.SaveChangesAsync();
Výsledkem je vložení následujícího řádku do databáze:
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Všimněte si, že složité typy nedostanou vlastní tabulky. Místo toho se ukládají vložené do sloupců Customers
tabulky. Odpovídá chování sdílení tabulek vlastněných typů.
Poznámka:
Neplánujeme povolit mapování složitých typů na vlastní tabulku. V budoucí verzi ale plánujeme, aby se složitý typ uložil jako dokument JSON v jednom sloupci. Pokud je to pro vás důležité, hlasujte pro problém č. 31252 .
Teď řekněme, že chceme odeslat objednávku zákazníkovi a jako výchozí fakturační adresu použít adresu zákazníka. Přirozeným způsobem, jak to udělat, je zkopírovat Address
objekt z objektu do objektu Customer
Order
. Příklad:
customer.Orders.Add(
new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, });
await context.SaveChangesAsync();
U složitých typů to funguje podle očekávání a adresa se vloží do Orders
tabulky:
INSERT INTO [Orders] ([Contents], [CustomerId],
[BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1], [BillingAddress_Line2], [BillingAddress_PostCode],
[ShippingAddress_City], [ShippingAddress_Country], [ShippingAddress_Line1], [ShippingAddress_Line2], [ShippingAddress_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);
Zatím možná říkáte: "Ale já bych to mohl udělat s vlastněnými typy!" Sémantika typu entity vlastněných typů se ale rychle dostane do cesty. Spuštěním výše uvedeného kódu s vlastněnými typy se například zobrazí upozornění a pak dojde k chybě:
warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update)
An exception occurred in the database while saving changes for context type 'NewInEfCore8.ComplexTypesSample+CustomerContext'.
System.InvalidOperationException: Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
Důvodem je to, že se pro tři různé instance entit používá jedna instance Address
typu entity (se stejnou hodnotou skrytého klíče). Na druhé straně je povoleno sdílení stejné instance mezi komplexními vlastnostmi, a proto kód funguje očekávaným způsobem při použití složitých typů.
Konfigurace komplexních typů
Komplexní typy musí být v modelu nakonfigurovány buď pomocí atributů mapování, nebo voláním ComplexProperty
rozhraní API v OnModelCreating
. Komplexní typy nejsou zjištěny konvencí.
Typ Address
lze například nakonfigurovat pomocí příkazu ComplexTypeAttribute:
[ComplexType]
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Nebo v OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.ComplexProperty(e => e.Address);
modelBuilder.Entity<Order>(b =>
{
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Proměnlivost
V předchozím příkladu jsme skončili se stejnou Address
instancí použitou na třech místech. To je povolené a nezpůsobuje žádné problémy s EF Core při použití složitých typů. Sdílení instancí stejného typu odkazu však znamená, že pokud se změní hodnota vlastnosti instance, projeví se tato změna ve všech třech použitích. Například podle výše uvedeného příkladu změníme Line1
adresu zákazníka a uložíme změny:
customer.Address.Line1 = "Peacock Lodge";
await context.SaveChangesAsync();
Výsledkem je následující aktualizace databáze při použití SQL Serveru:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] = @p3
OUTPUT 1
WHERE [Id] = @p4;
Všimněte si, že se změnily všechny tři Line1
sloupce, protože všechny sdílejí stejnou instanci. To obvykle není to, co chceme.
Tip
Pokud by se adresy objednávek měly při změně adresy zákazníka automaticky změnit, zvažte mapování adresy jako typu entity. Order
a Customer
pak může bezpečně odkazovat na stejnou instanci adresy (která je nyní identifikována klíčem) prostřednictvím navigační vlastnosti.
Dobrým způsobem, jak se vypořádat s problémy, jako je tato, je nastavit typ jako neměnný. Tato neměnnost je často přirozená, když je typ vhodným kandidátem na komplexní typ. Obvykle například dává smysl poskytnout složitý nový Address
objekt, nikoli jen ztlumit, řekněme, zemi, zatímco zbytek zůstane stejný.
Odkaz i typy hodnot se dají měnit. V následujících částech se podíváme na některé příklady.
Odkazové typy jako komplexní typy
Immutable – třída
V předchozím příkladu jsme použili jednoduchou proměnlivou tabulku class
. Abychom zabránili problémům s náhodným změtněním popsanými výše, můžeme třídu nastavit jako neměnnou. Příklad:
public class Address
{
public Address(string line1, string? line2, string city, string country, string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}
public string Line1 { get; }
public string? Line2 { get; }
public string City { get; }
public string Country { get; }
public string PostCode { get; }
}
Tip
V jazyce C# 12 nebo vyšší je možné tuto definici třídy zjednodušit pomocí primárního konstruktoru:
public class Address(string line1, string? line2, string city, string country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}
Nyní není možné změnit Line1
hodnotu u existující adresy. Místo toho potřebujeme vytvořit novou instanci se změněnou hodnotou. Příklad:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Tentokrát se hovor SaveChangesAsync
aktualizuje jenom na adresu zákazníka:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Všimněte si, že i když je objekt Address neměnný a celý objekt byl změněn, EF stále sleduje změny jednotlivých vlastností, takže se aktualizují pouze sloupce se změněnými hodnotami.
Neměnný záznam
C# 9 zavedl typy záznamů, které usnadňují vytváření a používání neměnných objektů. Objekt lze například Address
vytvořit typ záznamu:
public record Address
{
public Address(string line1, string? line2, string city, string country, string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}
public string Line1 { get; init; }
public string? Line2 { get; init; }
public string City { get; init; }
public string Country { get; init; }
public string PostCode { get; init; }
}
Tip
Tuto definici záznamu lze zjednodušit pomocí primárního konstruktoru:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
Nahrazení proměnlivého objektu a volání SaveChanges
teď vyžaduje méně kódu:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Typy hodnot jako komplexní typy
Proměnlivá struktura
Jednoduchý proměnlivý typ hodnoty lze použít jako komplexní typ. Můžete například Address
definovat jako v struct
jazyce C#:
public struct Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Přiřazení objektu zákazníka Address
k vlastnostem expedice a fakturace Address
má za následek, že každá vlastnost získá kopii objektu Address
zákazníka, protože takto fungují typy hodnot. To znamená, že úprava Address
zákazníka nezmění instance expedice nebo fakturace Address
, takže proměnlivé struktury nemají stejné problémy se sdílením instancí, ke kterým dochází u proměnlivých tříd.
Proměnlivé struktury se však obecně v jazyce C# nedoporučuje, proto si před použitím velmi pečlivě pomyslete.
Immutable – struktura
Neměnné struktury fungují stejně jako složité typy, stejně jako neměnné třídy. Lze například definovat tak, Address
aby ho nebylo možné upravit:
public readonly struct Address(string line1, string? line2, string city, string country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}
Kód pro změnu adresy teď vypadá stejně jako při použití neměnné třídy:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Neměnný záznam struktury
V jazyce C# 10 byly zavedeny struct record
typy, které usnadňují vytváření a práci s neměnnými záznamy struktur, jako jsou záznamy neměnné třídy. Můžeme například definovat Address
jako neměnný záznam struktury:
public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode);
Kód pro změnu adresy teď vypadá stejně jako při použití neměnného záznamu třídy:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Vnořené komplexní typy
Komplexní typ může obsahovat vlastnosti jiných komplexních typů. Pojďme například použít komplexní Address
typ odsud společně se složitým typem PhoneNumber
a vnořit je do jiného komplexního typu:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
public record PhoneNumber(int CountryCode, long Number);
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
public required PhoneNumber MobilePhone { get; init; }
}
V této části používáme neměnné záznamy, protože to je vhodná shoda pro sémantiku našich složitých typů, ale vnoření složitých typů lze provést s libovolnou variantou typu .NET.
Poznámka:
Pro typ nepoužíváme primární konstruktor Contact
, protože EF Core zatím nepodporuje injektáž konstruktoru komplexních hodnot typů. Pokud je to pro vás důležité, hlasujte pro problém č. 31621 .
Contact
Přidáme jako vlastnost Customer
:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Contact Contact { get; set; }
public List<Order> Orders { get; } = new();
}
A PhoneNumber
jako vlastnosti Order
:
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required PhoneNumber ContactPhone { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Konfiguraci vnořených komplexních typů lze znovu dosáhnout pomocí ComplexTypeAttribute:
[ComplexType]
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
[ComplexType]
public record PhoneNumber(int CountryCode, long Number);
[ComplexType]
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
public required PhoneNumber MobilePhone { get; init; }
}
Nebo v OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>(
b =>
{
b.ComplexProperty(
e => e.Contact,
b =>
{
b.ComplexProperty(e => e.Address);
b.ComplexProperty(e => e.HomePhone);
b.ComplexProperty(e => e.WorkPhone);
b.ComplexProperty(e => e.MobilePhone);
});
});
modelBuilder.Entity<Order>(
b =>
{
b.ComplexProperty(e => e.ContactPhone);
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Dotazy
Vlastnosti složitých typů u typů entit se považují za jakoukoli jinou nevigační vlastnost typu entity. To znamená, že se při načtení typu entity vždy načtou. To platí i pro všechny vnořené vlastnosti komplexního typu. Například dotazování na zákazníka:
var customer = await context.Customers.FirstAsync(e => e.Id == customerId);
Při použití SQL Serveru se přeloží do následujícího SQL Serveru:
SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country],
[c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode],
[c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode],
[c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__customerId_0
Všimněte si dvou věcí z tohoto SQL:
- Vše se vrátí k naplnění zákazníka a všech vnořených
Contact
,Address
aPhoneNumber
komplexních typů. - Všechny komplexní hodnoty typu jsou uloženy jako sloupce v tabulce pro typ entity. Komplexní typy se nikdy nenamapují na samostatné tabulky.
Projekce
Složité typy lze projektovat z dotazu. Například výběr pouze dodací adresy z objednávky:
var shippingAddress = await context.Orders
.Where(e => e.Id == orderId)
.Select(e => e.ShippingAddress)
.SingleAsync();
Při použití SQL Serveru se to přeloží na následující:
SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1],
[o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[Id] = @__orderId_0
Všimněte si, že projekce komplexních typů nelze sledovat, protože objekty komplexního typu nemají žádnou identitu, kterou by bylo možné použít ke sledování.
Použití v predikátech
Členy komplexních typů lze použít v predikátech. Například vyhledání všech objednávek jdou do určitého města:
var city = "Walpole St Peter";
var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync();
To se na SQL Serveru přeloží na následující SQL Server:
SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].[BillingAddress_City], [o].[BillingAddress_Country],
[o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].[BillingAddress_PostCode],
[o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].[ShippingAddress_City],
[o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].[ShippingAddress_Line2],
[o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[ShippingAddress_City] = @__city_0
Úplnou instanci komplexního typu lze také použít v predikátech. Například vyhledání všech zákazníků s daným telefonním číslem:
var phoneNumber = new PhoneNumber(44, 7777555777);
var customersWithNumber = await context.Customers
.Where(
e => e.Contact.MobilePhone == phoneNumber
|| e.Contact.WorkPhone == phoneNumber
|| e.Contact.HomePhone == phoneNumber)
.ToListAsync();
To se při použití SQL Serveru přeloží na následující SQL:
SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], [c].[Contact_Address_Line1],
[c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].[Contact_HomePhone_CountryCode],
[c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], [c].[Contact_MobilePhone_Number],
[c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE ([c].[Contact_MobilePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_MobilePhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_WorkPhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_WorkPhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_HomePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_HomePhone_Number] = @__entity_equality_phoneNumber_0_Number)
Všimněte si, že rovnost se provádí rozšířením každého člena komplexního typu. To odpovídá komplexním typům, které nemají žádný klíč pro identitu, a proto se instance komplexního typu rovná jiné instanci komplexního typu, pouze pokud jsou všichni jejich členové rovni. To také odpovídá rovnosti definované rozhraním .NET pro typy záznamů.
Manipulace s komplexními hodnotami typů
EF8 poskytuje přístup ke sledování informací, jako jsou aktuální a původní hodnoty komplexních typů a zda byla hodnota vlastnosti upravena. Komplexní typy rozhraní API jsou rozšíření rozhraní API pro sledování změn, které se už používá pro typy entit.
Metody ComplexProperty
EntityEntry vrácení položky pro celý komplexní objekt. Pokud chcete například získat aktuální hodnotu Order.BillingAddress
:
var billingAddress = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.CurrentValue;
Volání Property
lze přidat pro přístup k vlastnosti komplexního typu. Pokud chcete například získat aktuální hodnotu pouze fakturačního psč:
var postCode = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.Property(e => e.PostCode)
.CurrentValue;
K vnořeným komplexním typům se přistupuje pomocí vnořených volání ComplexProperty
. Pokud například chcete získat město z vnořeného Address
objektu Contact
na Customer
:
var currentCity = context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.City)
.CurrentValue;
Další metody jsou k dispozici pro čtení a změnu stavu. Můžete například PropertyEntry.IsModified použít k nastavení vlastnosti komplexního typu podle změny:
context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.PostCode)
.IsModified = true;
Aktuální omezení
Složité typy představují významnou investici do zásobníku EF. V této verzi jsme nemohli dělat všechno, ale plánujeme uzavřít některé mezery v budoucí verzi. Pokud je pro vás některá z těchto omezení důležitá, nezapomeňte hlasovat (👍) o příslušných problémech Na GitHubu.
Komplexní omezení typů v EF8 zahrnují:
- Podporuje kolekce komplexních typů. (Problém č. 31237)
- Povolit, aby vlastnosti komplexního typu měly hodnotu null. (Problém č. 31376)
- Namapuje komplexní vlastnosti typu na sloupce JSON. (Problém č. 31252)
- Injektáž konstruktoru pro komplexní typy (Problém č. 31621)
- Přidání podpory počátečních dat pro komplexní typy (Problém č. 31254)
- Namapujte vlastnosti komplexního typu pro zprostředkovatele Cosmos. (Problém č. 31253)
- Implementujte komplexní typy pro databázi v paměti. (Problém č. 31464)
Primitivní kolekce
Trvalý dotaz při použití relačních databází je to, co dělat s kolekcemi primitivních typů; to znamená, že seznamy nebo pole celých čísel, datum a čas, řetězce atd. Pokud používáte PostgreSQL, můžete je snadno ukládat pomocí integrovaného typu pole PostgreSQL. Pro jiné databáze existují dva běžné přístupy:
- Vytvořte tabulku se sloupcem pro primitivní hodnotu typu a další sloupec, který bude fungovat jako cizí klíč, který propojí každou hodnotu s jejím vlastníkem kolekce.
- Serializovat primitivní kolekci do některého typu sloupce, který zpracovává databáze – například serializace do a z řetězce.
První možnost má výhody v mnoha situacích – na konci této části se na ni podíváme rychle. Nejedná se ale o přirozenou reprezentaci dat v modelu a pokud skutečně máte kolekci primitivního typu, může být druhá možnost efektivnější.
Od verze Preview 4 teď EF8 obsahuje integrovanou podporu druhé možnosti, která jako formát serializace používá JSON. Json je pro to vhodný, protože moderní relační databáze obsahují integrované mechanismy pro dotazování a manipulaci s JSON, takže sloupec JSON může být v případě potřeby efektivně považován za tabulku bez režie při skutečném vytváření této tabulky. Tyto stejné mechanismy umožňují předání formátu JSON v parametrech a následné použití podobným způsobem jako parametry s hodnotou tabulky v dotazech – o tom později.
Tip
Zde uvedený kód pochází z PrimitiveCollectionsSample.cs.
Primitivní vlastnosti kolekce
EF Core může mapovat libovolnou IEnumerable<T>
vlastnost, kde T
je primitivní typ, na sloupec JSON v databázi. To se provádí konvencí pro veřejné vlastnosti, které mají getter i setter. Například všechny vlastnosti v následujícím typu entity se mapují na sloupce JSON podle konvence:
public class PrimitiveCollections
{
public IEnumerable<int> Ints { get; set; }
public ICollection<string> Strings { get; set; }
public IList<DateOnly> Dates { get; set; }
public uint[] UnsignedInts { get; set; }
public List<bool> Booleans { get; set; }
public List<Uri> Urls { get; set; }
}
Poznámka:
Co znamená "primitivní typ" v tomto kontextu? V podstatě něco, co poskytovatel databáze ví, jak mapovat, pomocí nějakého druhu převodu hodnoty v případě potřeby. Například ve výše uvedeném typu entity jsou typy int
, string
, DateTime
DateOnly
a bool
všechny jsou zpracovávány bez převodu poskytovatelem databáze. SQL Server nemá nativní podporu pro nepodepsané inty nebo identifikátory URI, ale uint
jsou Uri
stále považovány za primitivní typy, protože existují integrované převaděče hodnot pro tyto typy.
Ef Core ve výchozím nastavení používá k uložení formátu JSON nekonstrukční typ řetězce Unicode, protože tím se chrání před ztrátou dat s velkými kolekcemi. V některých databázových systémech, jako je NAPŘÍKLAD SQL Server, ale určení maximální délky řetězce může zvýšit výkon. To spolu s další konfigurací sloupců lze provádět běžným způsobem. Příklad:
modelBuilder
.Entity<PrimitiveCollections>()
.Property(e => e.Booleans)
.HasMaxLength(1024)
.IsUnicode(false);
Nebo pomocí atributů mapování:
[MaxLength(2500)]
[Unicode(false)]
public uint[] UnsignedInts { get; set; }
Výchozí konfiguraci sloupce lze použít pro všechny vlastnosti určitého typu pomocí konfigurace modelu před konvencí. Příklad:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<List<DateOnly>>()
.AreUnicode(false)
.HaveMaxLength(4000);
}
Dotazy s primitivními kolekcemi
Pojďme se podívat na některé dotazy, které využívají kolekce primitivních typů. K tomu budeme potřebovat jednoduchý model se dvěma typy entit. První představuje britský veřejný dům nebo "hospodu":
public class Pub
{
public Pub(string name, string[] beers)
{
Name = name;
Beers = beers;
}
public int Id { get; set; }
public string Name { get; set; }
public string[] Beers { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
}
Typ Pub
obsahuje dvě primitivní kolekce:
Beers
je pole řetězců představujících značky piva, které jsou k dispozici v hospodě.DaysVisited
je seznam kalendářních dat, kdy byla hospoda navštívena.
Tip
Ve skutečné aplikaci by pravděpodobně bylo vhodnější vytvořit typ entity pro pivo a mít tabulku pro piva. Tady zobrazujeme primitivní kolekci, abychom ilustrovali, jak fungují. Ale nezapomeňte, že jen proto, že můžete modelovat něco jako primitivní kolekci, neznamená, že byste nutně měli.
Druhý typ entity představuje psí procházku v britské krajině:
public class DogWalk
{
public DogWalk(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public Terrain Terrain { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
public Pub ClosestPub { get; set; } = null!;
}
public enum Terrain
{
Forest,
River,
Hills,
Village,
Park,
Beach,
}
Stejně jako Pub
, DogWalk
obsahuje také kolekci navštívených dat a odkaz na nejbližší hospodu, protože víte, někdy pes potřebuje talíř piva po dlouhé procházce.
První dotaz, který použijeme k tomuto modelu, je jednoduchý Contains
dotaz, který vyhledá všechny procházky s jedním z několika různých terénů:
var terrains = new[] { Terrain.River, Terrain.Beach, Terrain.Park };
var walksWithTerrain = await context.Walks
.Where(e => terrains.Contains(e.Terrain))
.Select(e => e.Name)
.ToListAsync();
Tato možnost je již přeložena aktuálními verzemi EF Core vložením hodnot, které se mají vyhledat. Například při použití SQL Serveru:
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE [w].[Terrain] IN (1, 5, 4)
Tato strategie ale nefunguje dobře s ukládáním databázových dotazů do mezipaměti; Informace o problému najdete v tématu Oznámení EF8 Preview 4 na blogu .NET.
Důležité
Vkládání hodnot se provádí takovým způsobem, že neexistuje žádná šance na útok prostřednictvím injektáže SQL. Změna použití kódu JSON popsaného níže se týká výkonu a nic společného se zabezpečením.
Pro EF Core 8 je teď výchozím nastavením předat seznam terénů jako jediný parametr obsahující kolekci JSON. Příklad:
@__terrains_0='[1,5,4]'
Dotaz pak použije OpenJson
na SQL Serveru:
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__terrains_0) AS [t]
WHERE CAST([t].[value] AS int) = [w].[Terrain])
Nebo json_each
na SQLite:
SELECT "w"."Name"
FROM "Walks" AS "w"
WHERE EXISTS (
SELECT 1
FROM json_each(@__terrains_0) AS "t"
WHERE "t"."value" = "w"."Terrain")
Poznámka:
OpenJson
je k dispozici pouze na SQL Serveru 2016 (úroveň kompatibility 130) a novějším. Sql Serveru můžete říct, že používáte starší verzi, a to tak, že nakonfigurujete úroveň kompatibility jako součást UseSqlServer
. Příklad:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(
@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120));
Pojďme vyzkoušet jiný druh Contains
dotazu. V tomto případě vyhledáme hodnotu kolekce parametrů ve sloupci. Například všechny hospody, které zásobuje Heineken:
var beer = "Heineken";
var pubsWithHeineken = await context.Pubs
.Where(e => e.Beers.Contains(beer))
.Select(e => e.Name)
.ToListAsync();
Existující dokumentace z článku Co je nového v EF7 obsahuje podrobné informace o mapování JSON, dotazech a aktualizacích. Tato dokumentace se teď týká také SQLite.
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b]
WHERE [b].[value] = @__beer_0)
OpenJson
se teď používá k extrakci hodnot ze sloupce JSON, aby se každá hodnota shodovala s předaným parametrem.
Použití parametru můžeme kombinovat OpenJson
se sloupcem OpenJson
. Pokud například chcete najít hospody, které mají na skladě některou z různých prodlev:
var beers = new[] { "Carling", "Heineken", "Stella Artois", "Carlsberg" };
var pubsWithLager = await context.Pubs
.Where(e => beers.Any(b => e.Beers.Contains(b)))
.Select(e => e.Name)
.ToListAsync();
To se na SQL Serveru přeloží na následující položky:
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__beers_0) AS [b]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b0]
WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL)))
Hodnota parametru @__beers_0
je ["Carling","Heineken","Stella Artois","Carlsberg"]
zde .
Pojďme se podívat na dotaz, který využívá sloupec obsahující kolekci kalendářních dat. Například pokud chcete najít hospody navštívené letos:
var thisYear = DateTime.Now.Year;
var pubsVisitedThisYear = await context.Pubs
.Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
.Select(e => e.Name)
.ToListAsync();
To se na SQL Serveru přeloží na následující položky:
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d]
WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)
Všimněte si, že dotaz používá funkci specifickou pro DATEPART
datum, protože EF ví, že primitivní kolekce obsahuje kalendářní data. Nemusí to vypadat, ale to je ve skutečnosti opravdu důležité. Vzhledem k tomu, že EF ví, co je v kolekci, může vygenerovat odpovídající SQL pro použití zadaných hodnot s parametry, funkcemi, dalšími sloupci atd.
Znovu použijeme kolekci kalendářních dat, tentokrát pro správné řazení hodnot typu a projektu extrahovaných z kolekce. Pojďme například vypsat hospody v pořadí, v jakém byli poprvé navštíveni, a s prvním a posledním datem, kdy byla každá hospoda navštívena:
var pubsVisitedInOrder = await context.Pubs
.Select(e => new
{
e.Name,
FirstVisited = e.DaysVisited.OrderBy(v => v).First(),
LastVisited = e.DaysVisited.OrderByDescending(v => v).First(),
})
.OrderBy(p => p.FirstVisited)
.ToListAsync();
To se na SQL Serveru přeloží na následující položky:
SELECT [p].[Name], (
SELECT TOP(1) CAST([d0].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d0]
ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], (
SELECT TOP(1) CAST([d1].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d1]
ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited]
FROM [Pubs] AS [p]
ORDER BY (
SELECT TOP(1) CAST([d].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d]
ORDER BY CAST([d].[value] AS date))
A nakonec, jak často skončíme návštěvou nejbližší hospody, když vezmeme psa na procházku? Pojďme se dozvědět:
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 na SQL Serveru přeloží na následující položky:
SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], (
SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
A zobrazí se následující data:
The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks.
The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks.
The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks.
The White Swan was visited 7 times in 9 "Woodnewton" walks.
The Eltisley was visited 6 times in 8 "Eltisley" walks.
Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks.
Farr Bay Inn was visited 7 times in 9 "Newlands" walks.
Vypadá to, že pivo a psí chůze jsou vítěznou kombinací!
Primitivní kolekce v dokumentech JSON
Ve všech výše uvedených příkladech sloupec pro primitivní kolekci obsahuje JSON. Nejedná se ale o mapování typu vlastněné entity na sloupec obsahující dokument JSON, který byl zaveden v EF7. Ale co když samotný dokument JSON obsahuje primitivní kolekci? Všechny výše uvedené dotazy stále fungují stejným způsobem! Představte si například, že přesuneme navštívená data do vlastněného typu Visits
namapovaného na dokument JSON:
public class Pub
{
public Pub(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public BeerData Beers { get; set; } = null!;
public Visits Visits { get; set; } = null!;
}
public class Visits
{
public string? LocationTag { get; set; }
public List<DateOnly> DaysVisited { get; set; } = null!;
}
Tip
Zde uvedený kód pochází z PrimitiveCollectionsInJsonSample.cs.
Teď můžeme spustit variantu konečného dotazu, který tentokrát extrahuje data z dokumentu JSON, včetně dotazů do primitivních kolekcí obsažených v dokumentu:
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
WalkLocationTag = w.Visits.LocationTag,
PubLocationTag = w.ClosestPub.Visits.LocationTag,
Count = w.Visits.DaysVisited.Count(v => w.ClosestPub.Visits.DaysVisited.Contains(v)),
TotalCount = w.Visits.DaysVisited.Count
}).ToListAsync();
To se na SQL Serveru přeloží na následující položky:
SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
A podobný dotaz při použití SQLite:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", (
SELECT COUNT(*)
FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d"
WHERE EXISTS (
SELECT 1
FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0"
WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
Tip
Všimněte si, že u SQLite EF Core se teď používá ->>
operátor, což vede k dotazům, které jsou čitelnější a často výkonnější.
Mapování primitivních kolekcí na tabulku
Výše jsme zmínili, že další možností primitivních kolekcí je namapovat je na jinou tabulku. Podpora první třídy je sledována problémem č. 25163. Pokud je pro vás důležité, nezapomeňte hlasovat pro tento problém. Do té doby, než se tento postup implementuje, je nejlepší vytvořit typ obtékání pro primitiv. Pojďme například vytvořit typ pro Beer
:
[Owned]
public class Beer
{
public Beer(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Všimněte si, že typ jednoduše zabalí primitivní hodnotu – nemá definovaný primární klíč ani žádné cizí klíče. Tento typ lze pak použít ve Pub
třídě:
public class Pub
{
public Pub(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public List<Beer> Beers { get; set; } = new();
public List<DateOnly> DaysVisited { get; private set; } = new();
}
EF teď vytvoří Beer
tabulku, která bude synchronizovat sloupce primárního klíče a cizího klíče zpět do Pubs
tabulky. Například na SQL Serveru:
CREATE TABLE [Beer] (
[PubId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]),
CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE
Vylepšení mapování sloupců JSON
EF8 obsahuje vylepšení podpory mapování sloupců JSON zavedených v EF7.
Tip
Zde uvedený kód pochází z JsonColumnsSample.cs.
Překlad přístupu k elementům do polí JSON
EF8 podporuje indexování v polích JSON při spouštění dotazů. Následující dotaz například zkontroluje, jestli byly provedeny první dvě aktualizace před daným datem.
var cutoff = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(365));
var updatedPosts = await context.Posts
.Where(
p => p.Metadata!.Updates[0].UpdatedOn < cutoff
&& p.Metadata!.Updates[1].UpdatedOn < cutoff)
.ToListAsync();
Při použití SQL Serveru se to přeloží do následujícího SQL Serveru:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0
AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0
Poznámka:
Tento dotaz bude úspěšný i v případě, že daný příspěvek nemá žádné aktualizace nebo má pouze jednu aktualizaci. V takovém případě JSON_VALUE
se vrátí NULL
a predikát se neshoduje.
Indexování do polí JSON lze také použít k projektování prvků z pole do konečných výsledků. Například následující projekty dotazu mají UpdatedOn
datum pro první a druhou aktualizaci každého příspěvku.
var postsAndRecentUpdatesNullable = await context.Posts
.Select(p => new
{
p.Title,
LatestUpdate = (DateOnly?)p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = (DateOnly?)p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();
Při použití SQL Serveru se to přeloží do následujícího SQL Serveru:
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]
Jak je uvedeno výše, vrátí hodnotu null, JSON_VALUE
pokud prvek pole neexistuje. To se zpracovává v dotazu přetypováním předpokládané hodnoty na hodnotu nullable DateOnly
. Alternativou k přetypování hodnoty je filtrování výsledků dotazu, JSON_VALUE
aby se nikdy nevracela hodnota null. Příklad:
var postsAndRecentUpdates = await context.Posts
.Where(p => p.Metadata!.Updates[0].UpdatedOn != null
&& p.Metadata!.Updates[1].UpdatedOn != null)
.Select(p => new
{
p.Title,
LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();
Při použití SQL Serveru se to přeloží do následujícího SQL Serveru:
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]
WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL)
AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL)
Překlad dotazů do vložených kolekcí
EF8 podporuje dotazy na kolekce primitivních (probíraných výše) i nemitivitivních typů vložených do dokumentu JSON. Následující dotaz například vrátí všechny příspěvky s libovolným seznamem hledaných termínů:
var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search #8", "Search #13", "Search #21", "Search #34" };
var postsWithSearchTerms = await context.Posts
.Where(post => post.Metadata!.TopSearches.Any(s => searchTerms.Contains(s.Term)))
.ToListAsync();
Při použití SQL Serveru se to přeloží do následujícího SQL Serveru:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM OPENJSON([p].[Metadata], '$.TopSearches') WITH (
[Count] int '$.Count',
[Term] nvarchar(max) '$.Term'
) AS [t]
WHERE EXISTS (
SELECT 1
FROM OPENJSON(@__searchTerms_0) WITH ([value] nvarchar(max) '$') AS [s]
WHERE [s].[value] = [t].[Term]))
Sloupce JSON pro SQLite
EF7 zavedla podporu mapování na sloupce JSON při použití Azure SQL/SQL Serveru. EF8 tuto podporu rozšiřuje na databáze SQLite. Pokud jde o podporu SQL Serveru, patří sem:
- Mapování agregací vytvořených z typů .NET na dokumenty JSON uložené ve sloupcích SQLite
- Dotazy do sloupců JSON, jako je filtrování a řazení podle prvků dokumentů
- Dotazy, které projektují prvky z dokumentu JSON do výsledků
- Aktualizace a ukládání změn do dokumentů JSON
Existující dokumentace z článku Co je nového v EF7 obsahuje podrobné informace o mapování JSON, dotazech a aktualizacích. Tato dokumentace se teď týká také SQLite.
Tip
Kód zobrazený v dokumentaci EF7 byl aktualizován tak, aby běžel také na SQLite, najdete v JsonColumnsSample.cs.
Dotazy do sloupců JSON
Pomocí funkce se dotazuje do sloupců JSON na SQLite json_extract
. Například dotaz "autoři v Chigley" z výše uvedené dokumentace:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
Při použití SQLite se přeloží na následující SQL:
SELECT "a"."Id", "a"."Name", "a"."Contact"
FROM "Authors" AS "a"
WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'
Aktualizace sloupců JSON
Pro aktualizace ef používá json_set
funkci na SQLite. Například při aktualizaci jedné vlastnosti v dokumentu:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
EF vygeneruje následující parametry:
info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']
Které používají json_set
funkci na SQLite:
UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]'))
WHERE "Id" = @p1
RETURNING 1;
HierarchyId v .NET a EF Core
Azure SQL a SQL Server mají speciální datový typ, hierarchyid
který se používá k ukládání hierarchických dat. V tomto případě "hierarchická data" v podstatě znamenají data, která tvoří strukturu stromu, kde každá položka může mít nadřazený objekt nebo podřízené položky. Mezi příklady těchto dat patří:
- Organizační struktura
- Systém souborů
- Sada úkolů v projektu
- Taxonomie jazykových termínů
- Graf odkazů mezi webovými stránkami
Databáze pak může spouštět dotazy na tato data pomocí své hierarchické struktury. Dotaz může například najít nadřazené položky a závislé položky nebo najít všechny položky v určité hloubkě v hierarchii.
Podpora v .NET a EF Core
Oficiální podpora pro typ SQL Serveru hierarchyid
přišla teprve nedávno na moderní platformy .NET (tj. .NET Core). Tato podpora je ve formě balíčku NuGet Microsoft.SqlServer.Types , který přináší typy specifické pro SQL Server nízké úrovně. V tomto případě se nazývá SqlHierarchyId
typ nízké úrovně .
Na další úrovni byl zaveden nový balíček Microsoft.EntityFrameworkCore.SqlServer.Abstractions , který obsahuje typ vyšší úrovně HierarchyId
určený pro použití v typech entit.
Tip
Typ HierarchyId
je více idiomatice pro normy .NET než SqlHierarchyId
, který je místo toho modelován po tom, jak jsou typy rozhraní .NET Framework hostovány uvnitř databázového stroje SQL Server. HierarchyId
je navržený tak, aby fungoval s EF Core, ale dá se použít i mimo EF Core v jiných aplikacích. Balíček Microsoft.EntityFrameworkCore.SqlServer.Abstractions
neodkazuje na žádné další balíčky, a proto má minimální dopad na nasazenou velikost a závislosti aplikace.
HierarchyId
Použití funkcí EF Core, jako jsou dotazy a aktualizace, vyžaduje balíček Microsoft.EntityFrameworkCore.SqlServer.HierarchyId. Tento balíček přináší Microsoft.EntityFrameworkCore.SqlServer.Abstractions
tranzitivní Microsoft.SqlServer.Types
závislosti a proto je často jediným potřebným balíčkem. Po instalaci balíčku je použití HierarchyId
povoleno voláním UseHierarchyId
v rámci volání UseSqlServer
aplikace . Příklad:
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
Poznámka:
Neoficiální podpora hierarchyid
pro EF Core je k dispozici již mnoho let prostřednictvím balíčku EntityFrameworkCore.SqlServer.HierarchyId . Tento balíček byl zachován jako spolupráce mezi komunitou a týmem EF. Teď, když je v .NET oficiální podpora hierarchyid
, kód z formulářů balíčků této komunity s oprávněním původních přispěvatelů, základem oficiálního balíčku popsaného zde. Mnoho díky všem, kteří se v průběhu let zapojili, včetně @aljones, @cutig3r, @huan086, @kmataru, @mehdihaghshenas a @vyrotek
Hierarchie modelování
Typ HierarchyId
lze použít pro vlastnosti typu entity. Předpokládejme například, že chceme modelovat paternální rodinný strom některých fiktivních poločasů. V typu entity pro Halfling
HierarchyId
lze vlastnost použít k vyhledání každé poloviny v rodinném stromu.
public class Halfling
{
public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null)
{
PathFromPatriarch = pathFromPatriarch;
Name = name;
YearOfBirth = yearOfBirth;
}
public int Id { get; private set; }
public HierarchyId PathFromPatriarch { get; set; }
public string Name { get; set; }
public int? YearOfBirth { get; set; }
}
Tip
Kód uvedený zde a v následujících příkladech pochází z HierarchyIdSample.cs.
Tip
V případě potřeby HierarchyId
je vhodný pro použití jako typ klíčové vlastnosti.
V tomto případě je rodinný strom kořenem identity rodiny. Každý poločas lze vysledovat ze stromu směrem dolů pomocí jeho PathFromPatriarch
vlastnosti. SQL Server pro tyto cesty používá kompaktní binární formát, ale při práci s kódem je běžné parsovat a od reprezentace řetězce čitelné pro člověka. V této reprezentaci je pozice na každé úrovni oddělena znakem /
. Představte si například rodinný strom v následujícím diagramu:
V tomto stromu:
- Balbo je v kořeni stromu, reprezentovaný
/
. - Balbo má pět dětí reprezentovaných
/1/
,/2/
,/3/
,/4/
a/5/
. - První dítě Balbo, Mungo, má také pět dětí, reprezentované
/1/1/
,/1/2/
,/1/3/
,/1/4/
a/1/5/
. Všimněte si, žeHierarchyId
pro Balbo (/1/
) je předpona pro všechny jeho děti. - Podobně, Balbo třetí dítě, Ponto, má dvě děti, reprezentované
/3/1/
a/3/2/
. Opět každý z těchto dětí je předponouHierarchyId
pro Ponto, který je reprezentován jako/3/
. - A tak dál dolů po stromě...
Následující kód vloží tento rodinný strom do databáze pomocí EF Core:
await AddRangeAsync(
new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));
await SaveChangesAsync();
Tip
V případě potřeby lze desetinné hodnoty použít k vytvoření nových uzlů mezi dvěma existujícími uzly. Například /3/2.5/2/
jde mezi /3/2/2/
a /3/3/2/
.
Dotazování hierarchií
HierarchyId
zveřejňuje několik metod, které lze použít v dotazech LINQ.
metoda | Popis |
---|---|
GetAncestor(int n) |
Získá úrovně uzlu n hierarchický strom. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
Získá hodnotu potomkového uzlu, který je větší než child1 a menší než child2 . |
GetLevel() |
Získá úroveň tohoto uzlu v hierarchickém stromu. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Získá hodnotu představující umístění nového uzlu, který má cestu od newRoot stejné cesty k této cestě oldRoot , a efektivně ji přesune do nového umístění. |
IsDescendantOf(HierarchyId? parent) |
Získá hodnotu určující, zda tento uzel je potomkem parent . |
Kromě toho lze operátory ==
, , <
!=
, <=
>
a >=
lze použít.
Tady jsou příklady použití těchto metod v dotazech LINQ.
Získání entit na dané úrovni ve stromu
Následující dotaz používá GetLevel
k vrácení všech polovin na dané úrovni v rodinném stromu:
var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
To se přeloží na následující SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
Když to spustíme ve smyčce, můžeme získat poloviny pro každou generaci:
Generation 0: Balbo
Generation 1: Mungo, Pansy, Ponto, Largo, Lily
Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
Generation 5: Angelica
Získání přímého předka entity
Následující dotaz používá GetAncestor
k vyhledání přímého předka poloviny, vzhledem k tomu, že název poloviny:
async Task<Halfling?> FindDirectAncestor(string name)
=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name == name).PathFromPatriarch
.GetAncestor(1));
To se přeloží na následující SQL:
SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch] = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0).GetAncestor(1)
Spuštění tohoto dotazu pro polovinu "Bilbo" vrátí "Bungo".
Získánípřímýchch
Následující dotaz také používá GetAncestor
, ale tentokrát k nalezení přímých sestupných napůl, vzhledem k tomu, že název poloviny:
IQueryable<Halfling> FindDirectDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);
To se přeloží na následující SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0)
Spuštění tohoto dotazu pro polovinu "Mungo" vrátí "Bungo", "Belba", "Longo" a "Linda".
Získání všech předků entity
GetAncestor
je užitečné pro vyhledávání na jedné nebo nižší úrovni, nebo skutečně zadaného počtu úrovní. Na druhé straně IsDescendantOf
je užitečné pro vyhledání všech předků nebo závislých. Například následující dotaz používá IsDescendantOf
k vyhledání všech předků poloviny, vzhledem k jeho názvu:
IQueryable<Halfling> FindAllAncestors(string name)
=> context.Halflings.Where(
ancestor => context.Halflings
.Single(
descendent =>
descendent.Name == name
&& ancestor.Id != descendent.Id)
.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel());
Důležité
IsDescendantOf
vrátí hodnotu true pro sebe, což je důvod, proč je filtrován v dotazu výše.
To se přeloží na následující SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
Spuštění tohoto dotazu pro polovinu "Bilbo" vrátí "Bungo", "Mungo" a "Balbo".
Získání všech sestupných hodnot entity
Následující dotaz také používá IsDescendantOf
, ale tentokrát na všechny sestupné napůl, vzhledem k názvu tohoto polovičního:
IQueryable<Halfling> FindAllDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings
.Single(
ancestor =>
ancestor.Name == name
&& descendent.Id != ancestor.Id)
.PathFromPatriarch))
.OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());
To se přeloží na následující SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].IsDescendantOf((
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel()
Spuštění tohoto dotazu pro polovinu "Mungo" vrátí "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho" a "Poppy".
Nalezení společného předka
Jednou z nejčastějších otázek týkajících se tohoto konkrétního rodinného stromu je, "kdo je společný předek Frodo a Bilbo?" Tento dotaz můžeme použít IsDescendantOf
k zápisu tohoto dotazu:
async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)
=> await context.Halflings
.Where(
ancestor => first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
&& second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel())
.FirstOrDefaultAsync();
To se přeloží na následující SQL:
SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
Spuštění tohoto dotazu s "Bilbo" a "Frodo" nám říká, že jejich společný předek je "Balbo".
Aktualizace hierarchií
K aktualizaci hierarchyid
sloupců lze použít normální mechanismy sledování změn a SaveChanges.
Opětovné závorky pod hierarchií
Jsem si například jistý, že všichni pamatujeme na skandál SR 1752 (a.k.a. "LongoGate"), když dna testování ukázalo, že Longo nebyl ve skutečnosti syn Mungo, ale ve skutečnosti syn Ponto! Jedním z pádů z tohoto skandálu bylo, že rodinný strom musel být znovu napsán. Zejména Longo a všechny jeho sestupné položky musely být znovu nadřazené z Mungo do Ponto. GetReparentedValue
se dá použít k tomu. Například první "Longo" a všechny jeho sestupné hodnoty jsou dotazovány:
var longoAndDescendents = await context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
.ToListAsync();
Pak GetReparentedValue
se používá k aktualizaci HierarchyId
pro Longo a každé sestupné, následované voláním SaveChangesAsync
:
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
Výsledkem je následující aktualizace databáze:
SET NOCOUNT ON;
UPDATE [Halflings] SET [PathFromPatriarch] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Halflings] SET [PathFromPatriarch] = @p2
OUTPUT 1
WHERE [Id] = @p3;
UPDATE [Halflings] SET [PathFromPatriarch] = @p4
OUTPUT 1
WHERE [Id] = @p5;
Pomocí těchto parametrů:
@p1='9',
@p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
@p3='16',
@p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
@p5='23',
@p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)
Poznámka:
Hodnoty parametrů vlastností HierarchyId
se posílají do databáze v kompaktním binárním formátu.
Po aktualizaci vrátí dotazování na sestupné hodnoty "Mungo" "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco" a "Poppy", zatímco dotazování na descendenty "Ponto" vrátí "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony" a "Angelica".
Nezpracované dotazy SQL pro nemapované typy
EF7 zavedl nezpracované dotazy SQL vracející skalární typy. Tato funkce je v EF8 vylepšená tak, aby zahrnovala nezpracované dotazy SQL vracející jakýkoli mapovatelný typ CLR bez zahrnutí tohoto typu do modelu EF.
Tip
Zde uvedený kód pochází z RawSqlSample.cs.
Dotazy používající nemapované typy se spouštějí pomocí SqlQuery nebo SqlQueryRaw. První používá interpolaci řetězců k parametrizaci dotazu, což pomáhá zajistit, aby všechny hodnoty, které nejsou konstantní, byly parametrizovány. Představte si například následující tabulku databáze:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Content] nvarchar(max) NOT NULL,
[PublishedOn] date NOT NULL,
[BlogId] int NOT NULL,
);
SqlQuery
lze použít k dotazování na tuto tabulku a vrácení instancí BlogPost
typu s vlastnostmi odpovídajícími sloupcům v tabulce:
Příklad:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateOnly PublishedOn { get; set; }
public int BlogId { get; set; }
}
Příklad:
var start = new DateOnly(2022, 1, 1);
var end = new DateOnly(2023, 1, 1);
var postsIn2022 =
await context.Database
.SqlQuery<BlogPost>($"SELECT * FROM Posts as p WHERE p.PublishedOn >= {start} AND p.PublishedOn < {end}")
.ToListAsync();
Tento dotaz je parametrizován a proveden takto:
SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1
Typ použitý pro výsledky dotazu může obsahovat běžné konstruktory mapování podporované ef Core, jako jsou parametrizované konstruktory a atributy mapování. Příklad:
public class BlogPost
{
public BlogPost(string blogTitle, string content, DateOnly publishedOn)
{
BlogTitle = blogTitle;
Content = content;
PublishedOn = publishedOn;
}
public int Id { get; private set; }
[Column("Title")]
public string BlogTitle { get; set; }
public string Content { get; set; }
public DateOnly PublishedOn { get; set; }
public int BlogId { get; set; }
}
Poznámka:
Typy používané tímto způsobem nemají definované klíče a nemohou mít relace s jinými typy. Typy s relacemi musí být mapovány v modelu.
Použitý typ musí mít vlastnost pro každou hodnotu v sadě výsledků, ale nemusí odpovídat žádné tabulce v databázi. Například následující typ představuje pouze podmnožinu informací pro každý příspěvek a obsahuje název blogu, který pochází z Blogs
tabulky:
public class PostSummary
{
public string BlogName { get; set; } = null!;
public string PostTitle { get; set; } = null!;
public DateOnly? PublishedOn { get; set; }
}
A dotazovat se můžete stejným SqlQuery
způsobem jako předtím:
var cutoffDate = new DateOnly(2022, 1, 1);
var summaries =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id
WHERE p.PublishedOn >= {cutoffDate}")
.ToListAsync();
Jednou z pěkných SqlQuery
funkcí je, že vrací IQueryable
, která se dá skládat pomocí LINQ. Do výše uvedeného dotazu lze například přidat klauzuli Where:
var summariesIn2022 =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
Provede se takto:
SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn]
FROM (
SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id
) AS [n]
WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2
V tomto okamžiku stojí za to si uvědomit, že všechny výše uvedené možnosti je možné provádět zcela v LINQ, aniž by bylo nutné psát sql. To zahrnuje vrácení instancí nemapovaného typu, jako je PostSummary
. Například předchozí dotaz může být napsán v LINQ jako:
var summaries =
await context.Posts.Select(
p => new PostSummary
{
BlogName = p.Blog.Name,
PostTitle = p.Title,
PublishedOn = p.PublishedOn,
})
.Where(p => p.PublishedOn >= start && p.PublishedOn < end)
.ToListAsync();
To znamená mnohem čistější SQL:
SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn]
FROM [Posts] AS [p]
INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id]
WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1
Tip
EF dokáže generovat čistější SQL, když je zodpovědný za celý dotaz, než je při psaní přes uživatelem zadaný SQL, protože v prvním případě je pro EF k dispozici úplná sémantika dotazu.
Zatím se všechny dotazy spouštěly přímo proti tabulkám. SqlQuery
lze také použít k vrácení výsledků ze zobrazení bez mapování typu zobrazení v modelu EF. Příklad:
var summariesFromView =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM PostAndBlogSummariesView")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
SqlQuery
Podobně je možné použít výsledky funkce:
var summariesFromFunc =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
.Where(p => p.PublishedOn < end)
.ToListAsync();
IQueryable
Vrácená hodnota se může skládat, když je výsledkem zobrazení nebo funkce, stejně jako výsledek dotazu tabulky. Uložené procedury lze provádět také pomocí SqlQuery
, ale většina databází nepodporuje jejich vytváření. Příklad:
var summariesFromStoredProc =
await context.Database.SqlQuery<PostSummary>(
@$"exec GetRecentPostSummariesProc")
.ToListAsync();
Vylepšení opožděného načítání
Opožděné načítání pro dotazy bez sledování
EF8 přidává podporu opožděného načítání navigace u entit, které nejsou sledovány nástrojem DbContext
. To znamená, že za dotazem bez sledování může následovat opožděné načítání navigace na entitách vrácených dotazem bez sledování.
Tip
Kód níže uvedených příkladů opožděného načítání pochází z LazyLoadingSample.cs.
Představte si například dotaz bez sledování pro blogy:
var blogs = await context.Blogs.AsNoTracking().ToListAsync();
Pokud Blog.Posts
je nakonfigurované opožděné načítání, například pomocí opožděných proxy serverů, pak přístup Posts
způsobí načtení z databáze:
Console.WriteLine();
Console.Write("Choose a blog: ");
if (int.TryParse(ReadLine(), out var blogId))
{
Console.WriteLine("Posts:");
foreach (var post in blogs[blogId - 1].Posts)
{
Console.WriteLine($" {post.Title}");
}
}
EF8 také hlásí, zda je daná navigace načtena pro entity, které nejsou sledovány kontextem. Příklad:
foreach (var blog in blogs)
{
if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
{
Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
}
}
Při používání opožděného načítání tímto způsobem je potřeba vzít v úvahu několik důležitých aspektů:
- Opožděné načítání bude úspěšné pouze do doby, než
DbContext
se použije k dotazování entity. - Entity dotazované tímto způsobem udržují odkaz na jejich
DbContext
, i když je nesledují. Je třeba dbát na to, aby se zabránilo nevrácení paměti v případě, že instance entit budou mít dlouhou dobu životnosti. - Explicitní odpojení entity nastavením jeho stavu na
EntityState.Detached
sever odkaz naDbContext
a opožděné načítání už nebude fungovat. - Mějte na paměti, že všechny opožděné načítání používají synchronní vstupně-výstupní operace, protože neexistuje způsob, jak přistupovat k vlastnosti asynchronním způsobem.
Opožděné načítání z nesledovaných entit funguje jak pro opožděné načítání proxy serverů , tak opožděné načítání bez proxy serverů.
Explicitní načítání z nesledovaných entit
EF8 podporuje načítání navigace u nesledovaných entit, i když není entita nebo navigace nakonfigurovaná pro opožděné načítání. Na rozdíl od opožděného načítání je možné toto explicitní načítání provádět asynchronně. Příklad:
await context.Entry(blog).Collection(e => e.Posts).LoadAsync();
Odhlášení z opožděného načítání pro konkrétní navigace
EF8 umožňuje konfiguraci konkrétních navigačních panelů, aby se nelíno načítá, i když je pro to všechno ostatní nastavené. Pokud chcete například nakonfigurovat Post.Author
navigaci tak, aby nebyla opožděná, postupujte takto:
modelBuilder
.Entity<Post>()
.Navigation(p => p.Author)
.EnableLazyLoading(false);
Zakázání opožděného načítání, jako je toto, funguje u opožděných proxy i opožděných načítání bez proxy serverů.
Opožděné načítání proxy serverů funguje přepsáním vlastností virtuální navigace. V klasických aplikacích EF6 se běžný zdroj chyb zapomene vytvořit virtuální navigaci, protože navigace se pak bezobslužně nenačte. Proto proxy servery EF Core ve výchozím nastavení vyvolá, když navigace není virtuální.
V EF8 to můžete změnit tak, aby se přihlásil k klasickému chování EF6 tak, aby navigace nebyla opožděná a nenačítá se jednoduše tak, že navigace není virtuální. Tento výslovný souhlas je nakonfigurován jako součást volání UseLazyLoadingProxies
. Příklad:
optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());
Přístup ke sledovaným entitě
Sledované entity vyhledávání podle primárního, alternativního nebo cizího klíče
Ef interně udržuje datové struktury pro hledání sledovaných entit podle primárního, alternativního nebo cizího klíče. Tyto datové struktury se používají k efektivní opravě mezi souvisejícími entitami, když se nové entity sledují nebo se změní relace.
EF8 obsahuje nová veřejná rozhraní API, aby aplikace mohly tyto datové struktury používat k efektivnímu vyhledávání sledovaných entit. K těmto rozhraním API se přistupuje prostřednictvím LocalView<TEntity> typu entity. Pokud chcete například vyhledat sledovaný entitu podle primárního klíče:
var blogEntry = context.Blogs.Local.FindEntry(2)!;
Tip
Zde uvedený kód pochází z LookupByKeySample.cs.
Metoda FindEntry
vrátí buď pro sledovaný entitu EntityEntry<TEntity> , nebo null
pokud se nesleduje žádná entita s daným klíčem. Stejně jako u všech metod LocalView
se databáze nikdy dotazuje, i když se entita nenajde. Vrácená položka obsahuje samotnou entitu a také informace o sledování. Příklad:
Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key {blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");
Vyhledání entity podle čehokoli jiného než primárního klíče vyžaduje, aby byl zadán název vlastnosti. Pokud například chcete vyhledat alternativní klíč:
var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new Uri("https://www.bricelam.net/"))!;
Nebo vyhledat jedinečný cizí klíč:
var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri), new Uri("https://www.bricelam.net/"))!;
Zatím vyhledávání vždy vrátilo jednu položku, nebo null
. Některé vyhledávání ale můžou vrátit více než jednu položku, například při vyhledávání ne jedinečným cizím klíčem. Metoda GetEntries
by se měla použít pro tato vyhledávání. Příklad:
var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);
Ve všech těchto případech je hodnota použitá pro vyhledávání buď primárním klíčem, alternativním klíčem nebo hodnotou cizího klíče. EF používá pro tato vyhledávání své interní datové struktury. Vyhledávání podle hodnoty lze však použít také pro hodnotu jakékoli vlastnosti nebo kombinace vlastností. Pokud například chcete najít všechny archivované příspěvky:
var archivedPostEntries = context.Posts.Local.GetEntries(nameof(Post.Archived), true);
Toto vyhledávání vyžaduje kontrolu všech sledovaných Post
instancí, takže bude méně efektivní než vyhledávání klíčů. Obvykle je však stále rychlejší než naivní dotazy používající ChangeTracker.Entries<TEntity>().
Nakonec je také možné provádět vyhledávání složených klíčů, jiné kombinace více vlastností nebo když typ vlastnosti není v době kompilace znám. Příklad:
var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new object[] { 4, "TagEF" });
Vytváření modelů
Diskriminující sloupce mají maximální délku
V EF8 jsou teď diskriminující sloupce řetězce používané pro mapování dědičnosti TPH nakonfigurované s maximální délkou. Tato délka se vypočítá jako nejmenší Fibonacciho číslo, které pokrývá všechny definované diskriminující hodnoty. Představte si například následující hierarchii:
public abstract class Document
{
public int Id { get; set; }
public string Title { get; set; }
}
public abstract class Book : Document
{
public string? Isbn { get; set; }
}
public class PaperbackEdition : Book
{
}
public class HardbackEdition : Book
{
}
public class Magazine : Document
{
public int IssueNumber { get; set; }
}
S konvencí použití názvů tříd pro nediskriminační hodnoty jsou zde možné hodnoty "PaperbackEdition", "HardbackEdition" a "Magazine", a proto je nediskriminační sloupec nakonfigurován pro maximální délku 21. Například při použití SQL Serveru:
CREATE TABLE [Documents] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Discriminator] nvarchar(21) NOT NULL,
[Isbn] nvarchar(max) NULL,
[IssueNumber] int NULL,
CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]),
Tip
Fibonacciho čísla se používají k omezení počtu vygenerování migrace, aby se při přidání nových typů do hierarchie změnila délka sloupce.
DatumOnly nebo TimeOnly podporované na SQL Serveru
Typy DateOnly a TimeOnly typy byly zavedeny v .NET 6 a byly podporovány pro několik poskytovatelů databází (např. SQLite, MySQL a PostgreSQL) od jejich zavedení. V případě SQL Serveru povolila nedávná verze balíčku Microsoft.Data.SqlClient , který cílí na .NET 6 , erikej přidat podporu pro tyto typy na úrovni ADO.NET. To zase zpevnělo způsob podpory v EF8 pro DateOnly
a TimeOnly
jako vlastnosti v typech entit.
Tip
DateOnly
a TimeOnly
lze ji použít v EF Core 6 a 7 pomocí komunitního balíčku ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly z @ErikEJ.
Představte si například následující model EF pro britské školy:
public class School
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly Founded { get; set; }
public List<Term> Terms { get; } = new();
public List<OpeningHours> OpeningHours { get; } = new();
}
public class Term
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly FirstDay { get; set; }
public DateOnly LastDay { get; set; }
public School School { get; set; } = null!;
}
[Owned]
public class OpeningHours
{
public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly? closesAt)
{
DayOfWeek = dayOfWeek;
OpensAt = opensAt;
ClosesAt = closesAt;
}
public DayOfWeek DayOfWeek { get; private set; }
public TimeOnly? OpensAt { get; set; }
public TimeOnly? ClosesAt { get; set; }
}
Tip
Zde uvedený kód pochází z DateOnlyTimeOnlySample.cs.
Poznámka:
Tento model představuje pouze britské školy a ukládá časy jako místní časy (GMT). Zpracování různých časových pásmů by tento kód výrazně komplikovalo. Všimněte si, že použití DateTimeOffset
zde nepomůže, protože čas otevření a uzavření má různé posuny v závislosti na tom, zda je letní čas aktivní nebo ne.
Tyto typy entit se při použití SQL Serveru mapují na následující tabulky. Všimněte si, že DateOnly
vlastnosti se mapuje na date
sloupce a TimeOnly
vlastnosti se mapuje na time
sloupce.
CREATE TABLE [Schools] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Founded] date NOT NULL,
CONSTRAINT [PK_Schools] PRIMARY KEY ([Id]));
CREATE TABLE [OpeningHours] (
[SchoolId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[DayOfWeek] int NOT NULL,
[OpensAt] time NULL,
[ClosesAt] time NULL,
CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]),
CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
CREATE TABLE [Term] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[FirstDay] date NOT NULL,
[LastDay] date NOT NULL,
[SchoolId] int NOT NULL,
CONSTRAINT [PK_Term] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
Dotazy používající DateOnly
a TimeOnly
fungují očekávaným způsobem. Například následující dotaz LINQ najde školy, které jsou aktuálně otevřené:
openSchools = await context.Schools
.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours.Any(
o => o.DayOfWeek == dayOfWeek
&& o.OpensAt < time && o.ClosesAt >= time))
.ToListAsync();
Tento dotaz se přeloží na následující SQL, jak ukazuje ToQueryString:
DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '19:53:40.4798052';
SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id], [o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt]
FROM [Schools] AS [s]
LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND [t].[LastDay] >= @__today_0) AND EXISTS (
SELECT 1
FROM [OpeningHours] AS [o]
WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND [o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2)
ORDER BY [s].[Id], [o0].[SchoolId]
DateOnly
a TimeOnly
dá se použít také ve sloupcích JSON. Můžete například OpeningHours
uložit jako dokument JSON, což vede k datům, která vypadají takto:
Column | Hodnota |
---|---|
ID | 2 |
Název | Farr High School |
Založený | 1964-05-01 |
Otevírání hodin | [ |
Když zkombinujeme dvě funkce z EF8, můžeme se teď dotazovat na otevírací dobu indexováním do kolekce JSON. Příklad:
openSchools = await context.Schools
.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours[(int)dayOfWeek].OpensAt < time
&& s.OpeningHours[(int)dayOfWeek].ClosesAt >= time)
.ToListAsync();
Tento dotaz se přeloží na následující SQL, jak ukazuje ToQueryString:
DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '20:14:34.7795877';
SELECT [s].[Id], [s].[Founded], [s].[Name], [s].[OpeningHours]
FROM [Schools] AS [s]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0
AND [t].[LastDay] >= @__today_0)
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].OpensAt') AS time) < @__time_2
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].ClosesAt') AS time) >= @__time_2
Nakonec lze aktualizace a odstranění provést pomocí sledování a saveChanges nebo pomocí ExecuteUpdate/ExecuteDelete. Příklad:
await context.Schools
.Where(e => e.Terms.Any(t => t.LastDay.Year == 2022))
.SelectMany(e => e.Terms)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LastDay, t => t.LastDay.AddDays(1)));
Tato aktualizace se přeloží na následující SQL:
UPDATE [t0]
SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay])
FROM [Schools] AS [s]
INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) = 2022)
Zpětná analýza Synapse a Dynamics 365 TDS
Reverzní analýza EF8 (generování uživatelského rozhraní z existující databáze) teď podporuje bezserverový fond SQL Synapse a databáze koncových bodů Dynamics 365 TDS.
Upozorňující
Tyto databázové systémy mají rozdíly od běžných databází SQL Serveru a Azure SQL. Tyto rozdíly znamenají, že při psaní dotazů nebo provádění jiných operací s těmito databázovými systémy se nepodporují všechny funkce EF Core.
Vylepšení matematických překladů
Obecná matematická rozhraní byla zavedena v .NET 7. Konkrétní typy jako double
a float
implementované tato rozhraní přidávají nová rozhraní, která zrcadlí stávající funkce matematiky a matematiky.
EF Core 8 překládá volání těchto obecných matematických rozhraní API v LINQ pomocí stávajících překladů SQL pro Math
a MathF
. To znamená, že si teď můžete vybrat mezi voláními, například Math.Sin
nebo double.Sin
v dotazech EF.
Ve spolupráci s týmem .NET jsme přidali dvě nové obecné matematické metody v .NET 8, které jsou implementovány do double
a float
. Ty se také překládají do SQL v EF Core 8.
.NET | SQL |
---|---|
DegreesToRadians | RADIANS |
RadiansToDegrees | DEGREES |
Nakonec jsme spolupracovali s EricEm Sinkem v projektu SQLitePCLRaw, abychom umožnili matematickým funkcím SQLite v jejich buildech nativní knihovny SQLite. To zahrnuje nativní knihovnu, kterou získáte ve výchozím nastavení při instalaci zprostředkovatele EF Core SQLite. To umožňuje několik nových překladů SQL v LINQ, mezi které patří: Acos, Acosh, Asin, Asinh, Atan, Atan2, Atanh, Ceiling, Cos, Cosh, DegreesToRadians, Exp, Floor, Log, Log2, Log10, Pow, RadiansToDegrees, Sign, Sin, Sinh, Sqrt, Tan, Tanh a Truncate.
Kontrola čekajících změn modelu
Přidali jsme nový dotnet ef
příkaz pro kontrolu, jestli se od poslední migrace provedly nějaké změny modelu. To může být užitečné ve scénářích CI/CD, abyste se ujistili, že jste vy nebo člen týmu nezapomněli přidat migraci.
dotnet ef migrations has-pending-model-changes
Tuto kontrolu můžete provést také programově v aplikaci nebo testech pomocí nové dbContext.Database.HasPendingModelChanges()
metody.
Vylepšení generování uživatelského rozhraní SQLite
SQLite podporuje pouze čtyři primitivní datové typy – INTEGER, REAL, TEXT a BLOB. Dříve to znamenalo, že když jste zpětnou analýzou databáze SQLite vygenerovali model EF Core, výsledné typy entit zahrnovaly pouze vlastnosti typu long
, double
, string
a byte[]
. Další typy .NET podporuje zprostředkovatel EF Core SQLite převodem mezi nimi a jedním ze čtyř primitivních typů SQLite.
V EF Core 8 teď kromě typu SQLite používáme kromě typu SQLite také datový formát a název typu sloupce k určení vhodnějšího typu .NET, který se má v modelu použít. Následující tabulky ukazují některé případy, kdy další informace vedou k lepším typům vlastností v modelu.
Název typu sloupce | Typ .NET |
---|---|
BOOLEOVSKÝ | |
SMALLINT | |
INT | |
BIGINT | long |
STRING |
Formát dat | Typ .NET |
---|---|
'0.0' | |
'1970-01-01' | |
'1970-01-01 00:00:00' | |
'00:00:00' | |
'00000000-0000-0000-0000-000000000000' |
Výchozí hodnoty služby Sentinel a databáze
Databáze umožňují konfigurovat sloupce tak, aby při vkládání řádku vygenerovaly výchozí hodnotu, pokud není k dispozici žádná hodnota. Toto je možné vyjádřit v ef pomocí HasDefaultValue
konstant:
b.Property(e => e.Status).HasDefaultValue("Hidden");
Nebo HasDefaultValueSql
pro libovolné klauzule SQL:
b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");
Tip
Níže uvedený kód pochází z DefaultConstraintSample.cs.
Aby ef mohl tuto funkci využít, musí určit, kdy a kdy neodesílat hodnotu sloupce. Ef ve výchozím nastavení používá jako sentinel výchozí modul CLR. To znamená, že když hodnota výše uvedené LeaseDate
hodnoty Status
jsou výchozími hodnotami CLR pro tyto typy, ef interpretuje to, že vlastnost nebyla nastavena, a proto neodesílá hodnotu do databáze. To funguje dobře pro odkazové typy– například pokud string
je null
vlastnost Status
, ef neodesílá null
do databáze, ale neobsahuje žádnou hodnotu, aby se použila výchozí ("Hidden"
) databáze. Stejně tak u DateTime
vlastnosti LeaseDate
EF nevloží výchozí hodnotu 1/1/0001 12:00:00 AM
CLR , ale místo toho tuto hodnotu vynechá, aby se použila výchozí hodnota databáze.
V některých případech je však výchozí hodnota CLR platnou hodnotou pro vložení. EF8 to zpracuje tak, že umožní, aby se hodnota sentinelu pro sloupec změnila. Představte si například celočíselnou hodnotu nakonfigurovanou s výchozí databází:
b.Property(e => e.Credits).HasDefaultValueSql(10);
V tomto případě chceme, aby byla nová entita vložena s daným počtem kreditů, pokud není zadána, v takovém případě je přiřazeno 10 kreditů. To však znamená, že vložení záznamu s nulovými kredity není možné, protože nulou je výchozí clr, a proto ef odešle žádnou hodnotu. V EF8 to lze opravit změnou sentinelu pro vlastnost z nuly na -1
:
b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);
Ef teď použije výchozí hodnotu databáze pouze v případě, že Credits
je nastavena na -1
hodnotu nula, bude vložena stejně jako jakákoli jiná částka.
Často může být užitečné ho odrážet v typu entity i v konfiguraci EF. Příklad:
public class Person
{
public int Id { get; set; }
public int Credits { get; set; } = -1;
}
To znamená, že hodnota sentinelu -1 se automaticky nastaví při vytvoření instance, což znamená, že vlastnost začíná ve stavu "not-set".
Tip
Pokud chcete nakonfigurovat výchozí omezení databáze pro použití při Migrations
vytváření sloupce, ale chcete, aby ef vždy vložil hodnotu, pak nakonfigurujte vlastnost tak, aby nebyla vygenerována. Například b.Property(e => e.Credits).HasDefaultValueSql(10).ValueGeneratedNever();
.
Výchozí hodnoty databáze pro logické hodnoty
Logické vlastnosti představují extrémní formu tohoto problému, protože clr výchozí (false
) je jedna z pouze dvou platných hodnot. To znamená, že bool
vlastnost s výchozím omezením databáze bude mít vloženou pouze hodnotu, pokud je true
tato hodnota . Pokud je false
výchozí hodnota databáze , znamená to, že pokud je false
hodnota vlastnosti , bude použita výchozí databáze, což je false
. V opačném případě, pokud je true
hodnota vlastnosti , bude true
vložena. Takže pokud je false
výchozí hodnota databáze , sloupec databáze skončí se správnou hodnotou.
Na druhou stranu, pokud je výchozí hodnota true
databáze , to znamená, když je false
hodnota vlastnosti , bude použita výchozí databáze, což je true
! A když je true
hodnota vlastnosti , bude true
vložena. Hodnota ve sloupci tedy vždy skončí true
v databázi bez ohledu na to, co je hodnota vlastnosti.
EF8 tento problém řeší nastavením sentinelu pro logické vlastnosti na stejnou hodnotu jako výchozí hodnota databáze. Oba případy výše pak vedou k vložení správné hodnoty bez ohledu na to, zda je true
false
výchozí nebo .
Tip
Při generování uživatelského rozhraní z existující databáze EF8 analyzuje a pak do volání zahrne jednoduché výchozí hodnoty HasDefaultValue
. (Dříve se všechny výchozí hodnoty vygenerovaly jako neprůžná HasDefaultValueSql
volání.) To znamená, že sloupce true
bez hodnoty null s výchozím nastavením nebo false
konstantní databází se už nevygenerují jako s možnou hodnotou null.
Výchozí hodnoty databáze pro výčty
Vlastnosti výčtu můžou mít podobné problémy s bool
vlastnostmi, protože výčty mají obvykle velmi malou sadu platných hodnot a clr výchozí může být jednou z těchto hodnot. Představte si například tento typ entity a výčet:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; }
}
public enum Level
{
Beginner,
Intermediate,
Advanced,
Unspecified
}
Vlastnost Level
se pak nakonfiguruje s výchozí databází:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate);
Při této konfiguraci EF vyloučí odesílání hodnoty do databáze, když je nastavena na Level.Beginner
hodnotu , a místo toho Level.Intermediate
je přiřazena databází. To není to, co bylo zamýšleno!
K problému by nedošlo, pokud byl výčt definován s "neznámou" nebo "nezadanou" hodnotou jako výchozí databáze:
public enum Level
{
Unspecified,
Beginner,
Intermediate,
Advanced
}
Není však vždy možné změnit existující výčt, takže v EF8 je možné znovu zadat sentinel. Například návrat k původnímu výčtu:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate)
.HasSentinel(Level.Unspecified);
Nyní Level.Beginner
se vloží jako normální a výchozí hodnota databáze se použije pouze v případě, že hodnota vlastnosti je Level.Unspecified
. Může být opět užitečné ho odrážet v samotném typu entity. Příklad:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; } = Level.Unspecified;
}
Použití backingového pole s možnou hodnotou null
Obecnější způsob zpracování problému popsaného výše je vytvoření pole s možnou hodnotou null pro vlastnost, která není nullable. Představte si například následující typ entity s bool
vlastností:
public class Account
{
public int Id { get; set; }
public bool IsActive { get; set; }
}
Vlastnost může mít záložní pole s možnou hodnotou null:
public class Account
{
public int Id { get; set; }
private bool? _isActive;
public bool IsActive
{
get => _isActive ?? false;
set => _isActive = value;
}
}
Pole pro zálohování zde zůstane, null
pokud se ve skutečnosti nevolá setter vlastnosti. To znamená, že hodnota backingového pole je lepší indikací, zda byla vlastnost nastavena nebo nebyla nastavena než CLR výchozí hodnota vlastnosti. Tento postup funguje s EF, protože EF použije backingové pole ke čtení a zápisu vlastnosti ve výchozím nastavení.
Better ExecuteUpdate a ExecuteDelete
Příkazy SQL, které provádějí aktualizace a odstranění, jako jsou například příkazy generované ExecuteUpdate
metodami ExecuteDelete
, musí cílit na jednoúčelovou tabulku databáze. V EF7 ExecuteUpdate
ale ExecuteDelete
nepodporuje aktualizace, které přistupují k více typům entit, i když dotaz nakonec ovlivnil jednu tabulku. EF8 toto omezení odebere. Představte si například typ entity s vlastním typem Customer
CustomerInfo
:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required CustomerInfo CustomerInfo { get; set; }
}
[Owned]
public class CustomerInfo
{
public string? Tag { get; set; }
}
Oba tyto typy entit se mapují na Customers
tabulku. Následující hromadná aktualizace však v EF7 selže, protože používá oba typy entit:
await context.Customers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(
s => s.SetProperty(b => b.CustomerInfo.Tag, "Tagged")
.SetProperty(b => b.Name, b => b.Name + "_Tagged"));
V EF8 se teď při použití Azure SQL přeloží na následující SQL:
UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
[c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0
Podobně lze instance vrácené z Union
dotazu aktualizovat, pokud se všechny aktualizace aktualizují ve stejné tabulce. Můžeme například aktualizovat libovolnou Customer
oblast s oblastí France
a zároveň všechny Customer
, kteří navštívili obchod s oblastí France
:
await context.CustomersWithStores
.Where(e => e.Region == "France")
.Union(context.Stores.Where(e => e.Region == "France").SelectMany(e => e.Customers))
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Tag, "The French Connection"));
V EF8 tento dotaz při použití Azure SQL vygeneruje následující:
UPDATE [c]
SET [c].[Tag] = N'The French Connection'
FROM [CustomersWithStores] AS [c]
INNER JOIN (
SELECT [c0].[Id], [c0].[Name], [c0].[Region], [c0].[StoreId], [c0].[Tag]
FROM [CustomersWithStores] AS [c0]
WHERE [c0].[Region] = N'France'
UNION
SELECT [c1].[Id], [c1].[Name], [c1].[Region], [c1].[StoreId], [c1].[Tag]
FROM [Stores] AS [s]
INNER JOIN [CustomersWithStores] AS [c1] ON [s].[Id] = [c1].[StoreId]
WHERE [s].[Region] = N'France'
) AS [t] ON [c].[Id] = [t].[Id]
Jako poslední příklad lze v EF8 použít k aktualizaci entit v hierarchii TPT, ExecuteUpdate
pokud jsou všechny aktualizované vlastnosti mapovány na stejnou tabulku. Představte si například tyto typy entit mapovaných pomocí TPT:
[Table("TptSpecialCustomers")]
public class SpecialCustomerTpt : CustomerTpt
{
public string? Note { get; set; }
}
[Table("TptCustomers")]
public class CustomerTpt
{
public int Id { get; set; }
public required string Name { get; set; }
}
U EF8 Note
je možné vlastnost aktualizovat:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));
Nebo může být vlastnost aktualizována Name
:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Ef8 se ale nepokusí aktualizovat vlastnosti i Name
Note
vlastnosti, protože jsou namapované na různé tabulky. Příklad:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted")
.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Vyvolá následující výjimku:
The LINQ expression 'DbSet<SpecialCustomerTpt>()
.Where(s => s.Name == __name_0)
.ExecuteUpdate(s => s.SetProperty<string>(
propertyExpression: b => b.Note,
valueExpression: "Noted").SetProperty<string>(
propertyExpression: b => b.Name,
valueExpression: b => b.Name + " (Noted)"))' could not be translated. Additional information: Multiple 'SetProperty' invocations refer to different tables ('b => b.Note' and 'b => b.Name'). A single 'ExecuteUpdate' call can only update the columns of a single table. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
Lepší použití IN
dotazů
Když se Contains
operátor LINQ používá s poddotazem, EF Core teď generuje lepší dotazy pomocí SQL IN
místo EXISTS
toho, aby vytvářel čitelnější SQL, v některých případech to může vést k výrazně rychlejším dotazům. Představte si například následující dotaz LINQ:
var blogsWithPosts = await context.Blogs
.Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
.ToListAsync();
EF7 vygeneruje pro PostgreSQL následující:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE EXISTS (
SELECT 1
FROM "Posts" AS p
WHERE p."BlogId" = b."Id")
Vzhledem k tomu, že poddotaz odkazuje na externí Blogs
tabulku (prostřednictvím b."Id"
), jedná se o korelovaný poddotaz, což znamená, že Posts
poddotaz musí být proveden pro každý řádek v Blogs
tabulce. V EF8 se místo toho vygeneruje následující SQL:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE b."Id" IN (
SELECT p."BlogId"
FROM "Posts" AS p
)
Vzhledem k tomu, že poddotaz již odkazuje Blogs
, lze jej vyhodnotit jednou, a to díky masivním vylepšením výkonu ve většině databázových systémů. Některé databázové systémy, zejména SQL Server, ale databáze dokáže optimalizovat první dotaz na druhý dotaz, aby byl výkon stejný.
Číselné verze řádků pro SQL Azure nebo SQL Server
Automatická optimistická souběžnost SQL Serveru se zpracovává pomocí rowversion
sloupců. A rowversion
je 8 bajtová neprůhláská hodnota předaná mezi databází, klientem a serverem. SqlClient ve výchozím nastavení zveřejňuje rowversion
typy jako byte[]
, navzdory proměnlivým referenčním typům je špatná shoda pro rowversion
sémantiku. V EF8 je snadné místo toho namapovat rowversion
sloupce na long
nebo ulong
vlastnosti. Příklad:
modelBuilder.Entity<Blog>()
.Property(e => e.RowVersion)
.IsRowVersion();
Odstranění závorek
Generování čitelného SQL je důležitým cílem EF Core. V EF8 je vygenerovaný SQL čitelnější díky automatickému odstranění nepotřebných závorek. Například následující dotaz LINQ:
await ctx.Customers
.Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName != null)
.ToListAsync();
Při použití EF7 se přeloží na následující Azure SQL:
SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ((([c].[Id] * 3) + 2) > 0 AND ([c].[FirstName] IS NOT NULL)) OR ([c].[LastName] IS NOT NULL)
To bylo při použití EF8 vylepšeno na následující:
SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ([c].[Id] * 3 + 2 > 0 AND [c].[FirstName] IS NOT NULL) OR [c].[LastName] IS NOT NULL
Konkrétní odhlášení pro klauzuli RETURNING/OUTPUT
EF7 změnila výchozí aktualizaci SQL tak, aby se používala RETURNING
/OUTPUT
pro načítání sloupců generovaných databází. V některých případech, kdy se zjistilo, kde to nefunguje, a ef8 zavádí explicitní nesouhlas s tímto chováním.
Pokud například chcete vyjádřit výslovný nesouhlas OUTPUT
s používáním SQL Serveru nebo poskytovatele Azure SQL:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlOutputClause(false));
Nebo se odhlásit RETURNING
při používání poskytovatele SQLite:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlReturningClause(false));
Jiné menší změny
Kromě výše popsaných vylepšení došlo k mnoha menším změnám EF8. Sem patří:
- Kompatibilita nativních funkcí AOT/oříznutí pro Microsoft.Data.Sqlite
- Povolení upřednostňovaných oblastí pro více oblastí nebo aplikací ve službě EF Core Cosmos
- SQLite: Přidejte EF. Functions.Unhex
- Možnosti indexu SQL Serveru SortInTempDB a DataCompression
- Povolit sdílení připojení mezi kontexty
- Přidání obecné verze atributu EntityTypeConfiguration
- Dotaz: Přidání podpory pro projektování entit JSON, které se skládají z
- Odebrání nepotřebných poddotazů a projekce při použití řazení bez omezení/posunu v nastavených operacích
- Povolit sdružování DbContext s jednoúčelovými službami
- Volitelné restartováníSequenceOperation.StartValue
- Povolit useSequence a HiLo u vlastností bez klíče
- Uveďte další informace, když se vygeneruje chyba No DbContext.
- Předání chování sledování dotazů do zachytávání materializace
- Porovnání řetězců nerozlišující malá a velká písmena na SQL Serveru
- Povolit převaděčům hodnot změnit dbType
- Řešení aplikačních služeb ve službách EF
- Povolit přenos vlastnictví DbConnection z aplikace do DbContext