Sdílet prostřednictvím


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, Guidstring, 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, PostCustomer. 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 , AddressCoordinate.

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 Addresszá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, Addressa PhoneNumber 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í:

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, DateTimeDateOnly 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á SqlHierarchyIdtyp 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í UseSqlServeraplikace . 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 HalflingHierarchyId 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:

Poločasový rodokmen

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, že HierarchyId 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ředponou HierarchyId 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 na DbContext 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 LocalViewse 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
[
{ "DayOfWeek": "Sunday", "ClosesAt": null, "OpensAt": null },
{ "DayOfWeek": "Monday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Tuesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Wednesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Thursday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Friday", "ClosesAt": "12:50:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Saturday", "ClosesAt": null, "OpensAt": null }
]

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, stringa 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Ý byte[]bool
SMALLINT dlouhýkrátký
INT dlouhéint
BIGINT long
STRING byte[]string
Formát dat Typ .NET
'0.0' řetězcovédesetinné číslo
'1970-01-01' stringDateOnly
'1970-01-01 00:00:00' stringDateTime
'00:00:00' StringTimeSpan
'00000000-0000-0000-0000-000000000000' identifikátor GUID řetězce

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 nullvlastnost 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 LeaseDateEF nevloží výchozí hodnotu 1/1/0001 12:00:00 AMCLR , 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 -1hodnotu 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 truetato hodnota . Pokud je falsevýchozí hodnota databáze , znamená to, že pokud je falsehodnota vlastnosti , bude použita výchozí databáze, což je false. V opačném případě, pokud je truehodnota vlastnosti , bude true vložena. Takže pokud je falsevýchozí hodnota databáze , sloupec databáze skončí se správnou hodnotou.

Na druhou stranu, pokud je výchozí hodnota truedatabáze , to znamená, když je falsehodnota vlastnosti , bude použita výchozí databáze, což je true! A když je truehodnota 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 falsevý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.Beginnerhodnotu , 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í Francea 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 EXISTStoho, 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ří: