Co nowego w programie EF Core 8
Program EF Core 8.0 (EF8) został wydany w listopadzie 2023 r.
Napiwek
Przykładowe przykłady można uruchamiać i debugować , pobierając przykładowy kod z usługi GitHub. Każda sekcja łączy się z kodem źródłowym specyficznym dla tej sekcji.
Program EF8 wymaga skompilowania zestawu .NET 8 SDK i wymaga uruchomienia środowiska uruchomieniowego platformy .NET 8. Program EF8 nie będzie działać we wcześniejszych wersjach platformy .NET i nie będzie działać w programie .NET Framework.
Obiekty wartości używające typów złożonych
Obiekty zapisane w bazie danych można podzielić na trzy szerokie kategorie:
- Obiekty, które nie mają struktury i przechowują pojedynczą wartość. Na przykład ,
int
,Guid
,string
,IPAddress
. Są to (nieco luźno) nazywane "typami pierwotnymi". - Obiekty, które mają strukturę przechowywania wielu wartości i gdzie tożsamość obiektu jest definiowana przez wartość klucza. Na przykład ,
Blog
,Post
,Customer
. Są one nazywane "typami jednostek". - Obiekty, które mają strukturę przechowywania wielu wartości, ale obiekt nie ma klucza definiującego tożsamość. Na przykład ,
Address
.Coordinate
Przed EF8 nie było dobrego sposobu mapowania trzeciego typu obiektu. Typy własności mogą być używane, ale ponieważ typy należące do nich są rzeczywiście typami jednostek, mają semantyka na podstawie wartości klucza, nawet jeśli ta wartość klucza jest ukryta.
Program EF8 obsługuje teraz "Typy złożone", aby pokryć ten trzeci typ obiektu. Obiekty typu złożonego:
- Nie są identyfikowane ani śledzone przez wartość klucza.
- Należy zdefiniować jako część typu jednostki. (Innymi słowy, nie można mieć
DbSet
typu złożonego). - Może to być typy wartości platformy .NET lub typy referencyjne.
- Wystąpienia mogą być współużytkowane przez wiele właściwości.
Prosty przykład
Rozważmy na przykład Address
typ:
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
następnie jest używany w trzech miejscach w prostym modelu klientów/zamówień:
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!;
}
Utwórzmy i zapiszmy klienta przy użyciu adresu:
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();
Spowoduje to wstawienie następującego wiersza do bazy danych:
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Zwróć uwagę, że typy złożone nie otrzymują własnych tabel. Zamiast tego są zapisywane w tekście w kolumnach Customers
tabeli. Jest to zgodne z zachowaniem udostępniania tabel należących do typów.
Uwaga
Nie planujemy zezwalać na mapowanie typów złożonych na własną tabelę. Jednak w przyszłej wersji planujemy zezwolić na zapisywanie typu złożonego jako dokument JSON w jednej kolumnie. Zagłosuj na problem nr 31252 , jeśli jest to dla Ciebie ważne.
Teraz załóżmy, że chcemy wysłać zamówienie do klienta i użyć adresu klienta jako domyślnego rozliczenia adresu wysyłkowego. Naturalnym sposobem wykonania tej czynności jest skopiowanie Address
obiektu z obiektu Customer
do obiektu Order
. Na przykład:
customer.Orders.Add(
new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, });
await context.SaveChangesAsync();
W przypadku typów złożonych działa to zgodnie z oczekiwaniami, a adres jest wstawiany do Orders
tabeli:
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);
Do tej pory można powiedzieć: "ale mógłbym to zrobić z typami własności!" Jednak semantyka "typu jednostki" typów własności szybko się w ten sposób. Na przykład uruchomienie powyższego kodu z typami własności powoduje wyświetlenie wysuwu ostrzeżeń, a następnie wyświetlenie błędu:
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()
Jest to spowodowane tym, że pojedyncze wystąpienie typu jednostki (z tą samą wartością klucza ukrytego Address
) jest używane dla trzech różnych wystąpień jednostek. Z drugiej strony współużytkowanie tego samego wystąpienia między złożonymi właściwościami jest dozwolone, a więc kod działa zgodnie z oczekiwaniami podczas korzystania z typów złożonych.
Konfiguracja typów złożonych
Typy złożone muszą być konfigurowane w modelu przy użyciu atrybutów mapowania lub przez wywołanie ComplexProperty
interfejsu API w programie OnModelCreating
. Typy złożone nie są odnajdywane zgodnie z konwencją.
Na przykład Address
typ można skonfigurować przy użyciu polecenia 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; }
}
Lub w pliku 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);
});
}
Możliwość mutowania
W powyższym przykładzie utworzyliśmy to samo Address
wystąpienie używane w trzech miejscach. Jest to dozwolone i nie powoduje żadnych problemów z programem EF Core podczas korzystania z typów złożonych. Jednak udostępnianie wystąpień tego samego typu odwołania oznacza, że jeśli wartość właściwości w wystąpieniu zostanie zmodyfikowana, ta zmiana zostanie odzwierciedlona we wszystkich trzech użyciach. Na przykład po wykonaniu powyższych czynności zmieńmy Line1
adres klienta i zapiszemy zmiany:
customer.Address.Line1 = "Peacock Lodge";
await context.SaveChangesAsync();
Spowoduje to następującą aktualizację bazy danych podczas korzystania z programu SQL Server:
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;
Zwróć uwagę, że wszystkie trzy Line1
kolumny uległy zmianie, ponieważ wszystkie współużytkują to samo wystąpienie. Zwykle nie jest to, czego chcemy.
Napiwek
Jeśli adresy zamówień powinny ulec zmianie automatycznie po zmianie adresu klienta, rozważ mapowanie adresu jako typu jednostki. Order
następnie Customer
można bezpiecznie odwoływać się do tego samego wystąpienia adresu (które jest teraz identyfikowane przez klucz) za pomocą właściwości nawigacji.
Dobrym sposobem radzenia sobie z takimi problemami jest uczynienie typu niezmiennym. Rzeczywiście, ta niezmienność jest często naturalna, gdy typ jest dobrym kandydatem do bycia typem złożonym. Na przykład zwykle warto dostarczyć złożony nowy Address
obiekt, a nie tylko wyciszyć, powiedzmy, kraj, pozostawiając resztę tak samo.
Zarówno typy odwołań, jak i wartości mogą być niezmienne. Przyjrzymy się kilku przykładom w poniższych sekcjach.
Typy referencyjne jako typy złożone
Niezmienna klasa
Użyliśmy prostego, modyfikowalnego class
w powyższym przykładzie. Aby zapobiec problemom z przypadkową mutacją opisaną powyżej, możemy uczynić klasę niezmienną. Na przykład:
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; }
}
Napiwek
W przypadku języka C# 12 lub nowszego można uprościć tę definicję klasy przy użyciu konstruktora podstawowego:
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;
}
Teraz nie można zmienić Line1
wartości na istniejącym adresie. Zamiast tego musimy utworzyć nowe wystąpienie ze zmienioną wartością. Na przykład:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Tym razem wywołanie , aby zaktualizować SaveChangesAsync
tylko adres klienta:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Należy pamiętać, że mimo że obiekt Address jest niezmienny, a cały obiekt został zmieniony, program EF nadal śledzi zmiany poszczególnych właściwości, więc tylko kolumny ze zmienionymi wartościami są aktualizowane.
Niezmienny rekord
Język C# 9 wprowadził typy rekordów, co ułatwia tworzenie i używanie niezmiennych obiektów. Na przykład Address
obiekt może być typu rekordu:
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; }
}
Napiwek
Tę definicję rekordu można uprościć przy użyciu konstruktora podstawowego:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
Zamiana obiektu modyfikowalnego i wywoływanie metody SaveChanges
wymaga teraz mniejszej ilości kodu:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Typy wartości jako typy złożone
Modyfikowalna struktura
Prosty typ wartości modyfikowalnej może być używany jako typ złożony. Na przykład Address
można zdefiniować jako element struct
w języku 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; }
}
Przypisanie obiektu klienta Address
do właściwości wysyłki i rozliczeń Address
powoduje pobranie kopii Address
obiektu , ponieważ jest to sposób działania typów wartości. Oznacza to, że modyfikowanie Address
elementu na kliencie nie spowoduje zmiany wystąpień wysyłki ani rozliczeń Address
, więc modyfikowalne struktury nie mają tych samych problemów z udostępnianiem wystąpień, które występują z klasami modyfikowalnymi.
Jednak modyfikowalne struktury są zwykle zniechęcane w języku C#, więc należy bardzo ostrożnie myśleć przed ich użyciem.
Niezmienna struktura
Niezmienne struktury działają dobrze jak złożone typy, podobnie jak w przypadku niezmiennych klas. Można na przykład zdefiniować taki sposób, Address
że nie można go modyfikować:
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;
}
Kod zmiany adresu wygląda teraz tak samo jak w przypadku używania niezmiennej klasy:
var currentAddress = customer.Address;
customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
await context.SaveChangesAsync();
Niezmienny rekord struktury
Wprowadzono struct record
typy języka C# 10, co ułatwia tworzenie i pracę z niezmiennymi rekordami struktury, takimi jak w przypadku niezmiennych rekordów klas. Na przykład możemy zdefiniować Address
jako niezmienny rekord struktury:
public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode);
Kod zmiany adresu wygląda teraz tak samo jak w przypadku używania niezmiennego rekordu klasy:
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Zagnieżdżone typy złożone
Typ złożony może zawierać właściwości innych typów złożonych. Na przykład użyjemy typu Address
złożonego z powyższego razem z typem złożonym i zagnieżdżmy je zarówno wewnątrz innego typu złożonego PhoneNumber
:
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; }
}
W tym miejscu używamy niezmiennych rekordów, ponieważ są one dobrym dopasowaniem do semantyki typów złożonych, ale zagnieżdżanie typów złożonych można wykonać z dowolną odmianą typu .NET.
Uwaga
Nie używamy podstawowego konstruktora dla Contact
typu, ponieważ program EF Core nie obsługuje jeszcze wstrzykiwania konstruktora złożonych wartości typów. Zagłosuj na problem nr 31621 , jeśli jest to dla Ciebie ważne.
Contact
Dodamy jako właściwość elementu 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();
}
I PhoneNumber
jako właściwości elementu 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!;
}
Konfigurację zagnieżdżonych typów złożonych można ponownie osiągnąć przy użyciu polecenia 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; }
}
Lub w pliku 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);
});
}
Zapytania
Właściwości typów złożonych w typach jednostek są traktowane jak każda inna właściwość nienawigacyjna typu jednostki. Oznacza to, że są one zawsze ładowane po załadowaniu typu jednostki. Dotyczy to również wszelkich zagnieżdżonych właściwości typu złożonego. Na przykład wykonywanie zapytań dotyczących klienta:
var customer = await context.Customers.FirstAsync(e => e.Id == customerId);
Podczas korzystania z programu SQL Server jest tłumaczony na następujący kod SQL:
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
Zwróć uwagę na dwie rzeczy z tego kodu SQL:
- Wszystko jest zwracane, aby wypełnić klienta i wszystkie zagnieżdżone
Contact
typy ,Address
iPhoneNumber
złożone. - Wszystkie złożone wartości typu są przechowywane jako kolumny w tabeli dla typu jednostki. Typy złożone nigdy nie są mapowane na oddzielne tabele.
Projekcje
Typy złożone mogą być projektowane na podstawie zapytania. Na przykład wybranie tylko adresu wysyłkowego z zamówienia:
var shippingAddress = await context.Orders
.Where(e => e.Id == orderId)
.Select(e => e.ShippingAddress)
.SingleAsync();
Przekłada się to na następujące kwestie podczas korzystania z programu SQL Server:
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
Należy pamiętać, że projekcje typów złożonych nie mogą być śledzone, ponieważ obiekty typu złożonego nie mają tożsamości do użycia do śledzenia.
Używanie w predykatach
Składowe typów złożonych mogą być używane w predykatach. Na przykład znalezienie wszystkich zamówień przechodzących do określonego miasta:
var city = "Walpole St Peter";
var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync();
Co przekłada się na następujący kod SQL w programie 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
W predykatach można również używać pełnego wystąpienia typu złożonego. Na przykład znalezienie wszystkich klientów z danym numerem telefonu:
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();
Przekłada się to na następujący kod SQL podczas korzystania z programu SQL Server:
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)
Zwróć uwagę, że równość jest wykonywana przez rozszerzenie każdego elementu członkowskiego typu złożonego. Jest to zgodne z typami złożonymi bez klucza tożsamości, dlatego wystąpienie typu złożonego jest równe innemu wystąpieniu typu złożonego, jeśli i tylko wtedy, gdy wszystkie ich elementy członkowskie są równe. Jest to również zgodne z równością zdefiniowaną przez platformę .NET dla typów rekordów.
Manipulowanie złożonymi wartościami typów
EF8 zapewnia dostęp do informacji śledzenia, takich jak bieżące i oryginalne wartości typów złożonych oraz czy wartość właściwości została zmodyfikowana. Typy złożone interfejsu API to rozszerzenie interfejsu API śledzenia zmian, które jest już używane dla typów jednostek.
ComplexProperty
Metody zwracania EntityEntry wpisu dla całego złożonego obiektu. Aby na przykład uzyskać bieżącą wartość elementu Order.BillingAddress
:
var billingAddress = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.CurrentValue;
Wywołanie metody Property
można dodać w celu uzyskania dostępu do właściwości typu złożonego. Aby na przykład uzyskać bieżącą wartość tylko kodu pocztowego rozliczeń:
var postCode = context.Entry(order)
.ComplexProperty(e => e.BillingAddress)
.Property(e => e.PostCode)
.CurrentValue;
Dostęp do zagnieżdżonych typów złożonych uzyskuje się przy użyciu zagnieżdżonych wywołań metody ComplexProperty
. Na przykład, aby pobrać miasto z zagnieżdżonego Address
Contact
obiektu na :Customer
var currentCity = context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.City)
.CurrentValue;
Inne metody są dostępne do odczytywania i zmieniania stanu. Na przykład PropertyEntry.IsModified można użyć do ustawienia właściwości typu złożonego zgodnie z modyfikacją:
context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.PostCode)
.IsModified = true;
Bieżące ograniczenia
Typy złożone reprezentują znaczną inwestycję w stos EF. Nie byliśmy w stanie wykonać wszystkich prac w tej wersji, ale planujemy zamknąć niektóre luki w przyszłej wersji. Pamiętaj, aby głosować (👍) na odpowiednie problemy z usługą GitHub, jeśli rozwiązanie któregokolwiek z tych ograniczeń jest dla Ciebie ważne.
Ograniczenia typów złożonych w programie EF8 obejmują:
- Obsługa kolekcji typów złożonych. (Problem nr 31237)
- Zezwalaj na wartości null właściwości typu złożonego. (Problem nr 31376)
- Mapuj właściwości typu złożonego na kolumny JSON. (Problem nr 31252)
- Wstrzykiwanie konstruktora dla typów złożonych. (Problem nr 31621)
- Dodaj obsługę danych inicjujnych dla typów złożonych. (Problem nr 31254)
- Mapuj właściwości typu złożonego dla dostawcy usługi Cosmos. (Problem nr 31253)
- Zaimplementuj złożone typy dla bazy danych w pamięci. (Problem nr 31464)
Kolekcje pierwotne
Trwałe pytanie podczas korzystania z relacyjnych baz danych to, co należy zrobić z kolekcjami typów pierwotnych; oznacza to, listy lub tablice liczb całkowitych, daty/godziny, ciągów itd. Jeśli używasz bazy danych PostgreSQL, możesz łatwo przechowywać te elementy przy użyciu wbudowanego typu tablicy PostgreSQL. W przypadku innych baz danych istnieją dwa typowe podejścia:
- Utwórz tabelę z kolumną dla wartości typu pierwotnego i inną kolumną, która będzie pełnić rolę klucza obcego łączącego każdą wartość z właścicielem kolekcji.
- Serializowanie kolekcji pierwotnej do typu kolumny obsługiwanego przez bazę danych — na przykład serializowanie do i z ciągu.
Pierwsza opcja ma zalety w wielu sytuacjach — przyjrzymy się jej na końcu tej sekcji. Jednak nie jest to naturalna reprezentacja danych w modelu, a jeśli to, co naprawdę masz, to kolekcja typu pierwotnego, druga opcja może być bardziej skuteczna.
Począwszy od wersji zapoznawczej 4, program EF8 zawiera teraz wbudowaną obsługę drugiej opcji, używając formatu JSON jako formatu serializacji. Kod JSON działa dobrze, ponieważ nowoczesne relacyjne bazy danych zawierają wbudowane mechanizmy wykonywania zapytań i manipulowania formatem JSON, dzięki czemu kolumna JSON może w razie potrzeby być traktowana jako tabela bez konieczności faktycznego tworzenia tej tabeli. Te same mechanizmy umożliwiają przekazywanie danych JSON w parametrach, a następnie ich używanie w podobny sposób do parametrów wartości tabeli w zapytaniach — więcej na ten temat później.
Napiwek
Pokazany tutaj kod pochodzi z PrimitiveCollectionsSample.cs.
Właściwości kolekcji pierwotnej
Program EF Core może mapować dowolną IEnumerable<T>
właściwość, gdzie T
jest typem pierwotnym, do kolumny JSON w bazie danych. Odbywa się to zgodnie z konwencją dla właściwości publicznych, które mają zarówno parametr getter, jak i setter. Na przykład wszystkie właściwości w następującym typie jednostki są mapowane na kolumny JSON według konwencji:
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; }
}
Uwaga
Co oznacza "typ pierwotny" w tym kontekście? Zasadniczo coś, co dostawca bazy danych wie, jak mapować, używając pewnego rodzaju konwersji wartości w razie potrzeby. Na przykład w typie jednostki powyżej typy int
, string
, DateOnly
DateTime
i bool
są obsługiwane bez konwersji przez dostawcę bazy danych. Program SQL Server nie ma natywnej obsługi niepodpisanych ints ani identyfikatorów URI, ale uint
Uri
nadal jest traktowany jako typy pierwotne, ponieważ istnieją wbudowane konwertery wartości dla tych typów.
Domyślnie program EF Core używa nieprzeciętnego typu kolumny ciągu Unicode do przechowywania kodu JSON, ponieważ chroni to przed utratą danych przy użyciu dużych kolekcji. Jednak w niektórych systemach baz danych, takich jak SQL Server, określenie maksymalnej długości ciągu może poprawić wydajność. Można to zrobić razem z inną konfiguracją kolumn w normalny sposób. Na przykład:
modelBuilder
.Entity<PrimitiveCollections>()
.Property(e => e.Booleans)
.HasMaxLength(1024)
.IsUnicode(false);
Możesz też użyć atrybutów mapowania:
[MaxLength(2500)]
[Unicode(false)]
public uint[] UnsignedInts { get; set; }
Domyślna konfiguracja kolumny może być używana dla wszystkich właściwości określonego typu przy użyciu konfiguracji modelu przed konwencją. Na przykład:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<List<DateOnly>>()
.AreUnicode(false)
.HaveMaxLength(4000);
}
Zapytania z kolekcjami pierwotnymi
Przyjrzyjmy się niektórym zapytaniom korzystającym z kolekcji typów pierwotnych. W tym celu będziemy potrzebować prostego modelu z dwoma typami jednostek. Pierwszy reprezentuje brytyjski dom publiczny lub "pub":
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
zawiera dwie kolekcje pierwotne:
Beers
to tablica ciągów reprezentujących marki piwa dostępne w pubie.DaysVisited
jest listą dat, w których odwiedzono pub.
Napiwek
W prawdziwej aplikacji prawdopodobnie bardziej sensowne byłoby utworzenie typu jednostki dla piwa i posiadanie tabeli dla piw. Przedstawiamy tutaj kolekcję pierwotną, aby zilustrować sposób ich działania. Pamiętaj jednak, że tylko dlatego, że można modelować coś jako kolekcję pierwotną, nie oznacza, że musisz.
Drugi typ jednostki reprezentuje pies spacer na brytyjskiej wsi:
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,
}
Podobnie jak Pub
, DogWalk
zawiera również kolekcję odwiedzonych dat i link do najbliższego pubu, ponieważ, wiesz, czasami pies potrzebuje spodka piwa po długim spacerze.
Korzystając z tego modelu, pierwszym zapytaniem, które wykonamy, jest proste Contains
zapytanie, które umożliwia znalezienie wszystkich spacerów z jednym z kilku różnych terenu:
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();
Jest to już tłumaczone przez bieżące wersje platformy EF Core przez podkreślenie wartości do wyszukania. Na przykład w przypadku korzystania z programu SQL Server:
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE [w].[Terrain] IN (1, 5, 4)
Jednak ta strategia nie działa dobrze w przypadku buforowania zapytań bazy danych; Zobacz Ogłoszenie programu EF8 (wersja zapoznawcza 4 ) na blogu platformy .NET, aby zapoznać się z omówieniem problemu.
Ważne
Podkreślenie wartości odbywa się w taki sposób, że nie ma szans na atak polegający na wstrzyknięciu kodu SQL. Zmiana użycia kodu JSON opisanego poniżej dotyczy wydajności i nic wspólnego z zabezpieczeniami.
W przypadku platformy EF Core 8 domyślną wartością jest teraz przekazanie listy terenu jako pojedynczego parametru zawierającego kolekcję JSON. Na przykład:
@__terrains_0='[1,5,4]'
Następnie zapytanie jest używane OpenJson
w programie SQL Server:
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])
Lub 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")
Uwaga
OpenJson
jest dostępna tylko w programie SQL Server 2016 (poziom zgodności 130) i nowszych wersjach. Możesz poinformować program SQL Server, że używasz starszej wersji, konfigurując poziom zgodności w ramach programu UseSqlServer
. Na przykład:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(
@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120));
Wypróbujmy inny rodzaj Contains
zapytania. W tym przypadku wyszukamy wartość kolekcji parametrów w kolumnie. Na przykład każdy pub, który zapasy Heineken:
var beer = "Heineken";
var pubsWithHeineken = await context.Pubs
.Where(e => e.Beers.Contains(beer))
.Select(e => e.Name)
.ToListAsync();
Istniejąca dokumentacja z nowości w programie EF7 zawiera szczegółowe informacje na temat mapowania, zapytań i aktualizacji JSON. Ta dokumentacja dotyczy teraz również programu SQLite.
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b]
WHERE [b].[value] = @__beer_0)
OpenJson
Element jest teraz używany do wyodrębniania wartości z kolumny JSON, aby można było dopasować każdą wartość do przekazanego parametru.
Możemy połączyć użycie parametru OpenJson
z parametrem OpenJson
w kolumnie . Aby na przykład znaleźć puby, które zaopatrzyją się w jedną z różnych lagerów:
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();
Przekłada się to na następujące elementy w programie SQL Server:
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)))
Wartość parametru @__beers_0
to ["Carling","Heineken","Stella Artois","Carlsberg"]
.
Przyjrzyjmy się zapytaniu, które korzysta z kolumny zawierającej kolekcję dat. Na przykład aby znaleźć puby odwiedzone w tym roku:
var thisYear = DateTime.Now.Year;
var pubsVisitedThisYear = await context.Pubs
.Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
.Select(e => e.Name)
.ToListAsync();
Przekłada się to na następujące elementy w programie SQL Server:
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)
Zwróć uwagę, że zapytanie korzysta z funkcji DATEPART
specyficznej dla daty, ponieważ program EF wie, że kolekcja pierwotna zawiera daty. To może nie wydawać się tak, ale jest to naprawdę ważne. Ponieważ program EF wie, co znajduje się w kolekcji, może wygenerować odpowiedni język SQL, aby używać typowanych wartości z parametrami, funkcjami, innymi kolumnami itp.
Użyjmy ponownie kolekcji dat, tym razem, aby odpowiednio zamówić wartości typu i projektu wyodrębnione z kolekcji. Na przykład wyświetlmy listę pubów w kolejności, w której zostały one po raz pierwszy odwiedzone, i z pierwszą i ostatnią datą, w której odwiedzono każdy pub:
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();
Przekłada się to na następujące elementy w programie SQL Server:
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))
I wreszcie, jak często odwiedzamy najbliższy pub, biorąc psa na spacer? Dowiedzmy się:
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();
Przekłada się to na następujące elementy w programie SQL Server:
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]
I ujawnia następujące dane:
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.
Wygląda na to, że piwo i pies chodzenie są zwycięską kombinacją!
Kolekcje pierwotne w dokumentach JSON
We wszystkich powyższych przykładach kolumna kolekcji pierwotnej zawiera kod JSON. Nie jest to jednak takie samo, jak mapowanie typu jednostki należącej do kolumny zawierającej dokument JSON, który został wprowadzony w programie EF7. Ale co zrobić, jeśli sam dokument JSON zawiera kolekcję pierwotną? Cóż, wszystkie powyższe zapytania nadal działają w ten sam sposób! Załóżmy na przykład, że przenosimy dni odwiedzone dane do typu Visits
należącego do dokumentu 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!;
}
Napiwek
Pokazany tutaj kod pochodzi z PrimitiveCollectionsInJsonSample.cs.
Teraz możemy uruchomić odmianę naszego końcowego zapytania, które tym razem wyodrębnia dane z dokumentu JSON, w tym zapytania do kolekcji pierwotnych zawartych w dokumencie:
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();
Przekłada się to na następujące elementy w programie SQL Server:
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]
Podobne zapytanie podczas korzystania z biblioteki 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"
Napiwek
Zwróć uwagę, że w środowisku SQLite EF Core jest teraz używany ->>
operator, co powoduje, że zapytania, które są łatwiejsze do odczytania i często bardziej wydajne.
Mapowanie kolekcji pierwotnych na tabelę
Wspomnieliśmy powyżej, że inną opcją kolekcji pierwotnych jest mapowania ich na inną tabelę. Obsługa pierwszej klasy dla tego problemu jest śledzona przez problem nr 25163; pamiętaj, aby głosować na ten problem, jeśli jest dla Ciebie ważne. Dopóki nie zostanie to zaimplementowane, najlepszym rozwiązaniem jest utworzenie typu opakowującego dla elementu pierwotnego. Na przykład utwórzmy typ dla elementu Beer
:
[Owned]
public class Beer
{
public Beer(string name)
{
Name = name;
}
public string Name { get; private set; }
}
Zwróć uwagę, że typ po prostu opakowuje wartość pierwotną — nie ma klucza podstawowego ani żadnych kluczy obcych zdefiniowanych. Ten typ może być następnie używany w Pub
klasie:
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();
}
Program EF utworzy teraz tabelę Beer
, synthesizing klucz podstawowy i kolumny klucza obcego Pubs
z powrotem do tabeli. Na przykład w programie SQL Server:
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
Ulepszenia mapowania kolumn JSON
Program EF8 zawiera ulepszenia obsługi mapowania kolumn JSON wprowadzone w programie EF7.
Napiwek
Pokazany tutaj kod pochodzi z JsonColumnsSample.cs.
Tłumaczenie dostępu do elementów na tablice JSON
Program EF8 obsługuje indeksowanie w tablicach JSON podczas wykonywania zapytań. Na przykład następujące zapytanie sprawdza, czy pierwsze dwie aktualizacje zostały wprowadzone przed daną datą.
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();
Przekłada się to na następujący kod SQL podczas korzystania z programu SQL Server:
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
Uwaga
To zapytanie powiedzie się, nawet jeśli dany wpis nie ma żadnych aktualizacji lub ma tylko jedną aktualizację. W takim przypadku JSON_VALUE
zwraca wartość , NULL
a predykat nie jest zgodny.
Indeksowanie do tablic JSON może być również używane do projekcji elementów z tablicy do wyników końcowych. Na przykład następujące zapytanie projektuje UpdatedOn
datę pierwszej i drugiej aktualizacji każdego wpisu.
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();
Przekłada się to na następujący kod SQL podczas korzystania z programu SQL Server:
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 wspomniano powyżej, zwraca wartość null, JSON_VALUE
jeśli element tablicy nie istnieje. Jest to obsługiwane w zapytaniu przez rzutowanie przewidywanej wartości na wartość null DateOnly
. Alternatywą do rzutowania wartości jest filtrowanie wyników zapytania tak, aby JSON_VALUE
nigdy nie zwracało wartości null. Na przykład:
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();
Przekłada się to na następujący kod SQL podczas korzystania z programu SQL Server:
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)
Tłumaczenie zapytań na kolekcje osadzone
Program EF8 obsługuje zapytania dotyczące kolekcji zarówno typów pierwotnych (omówionych powyżej) jak i nietypowych osadzonych w dokumencie JSON. Na przykład następujące zapytanie zwraca wszystkie wpisy z dowolną listą terminów wyszukiwania:
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();
Przekłada się to na następujący kod SQL podczas korzystania z programu SQL Server:
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]))
Kolumny JSON dla SQLite
Program EF7 wprowadził obsługę mapowania do kolumn JSON podczas korzystania z programu Azure SQL/SQL Server. Program EF8 rozszerza tę obsługę baz danych SQLite. Jeśli chodzi o obsługę programu SQL Server, obejmuje to:
- Mapowanie agregacji utworzonych na podstawie typów platformy .NET na dokumenty JSON przechowywane w kolumnach SQLite
- Zapytania dotyczące kolumn JSON, takich jak filtrowanie i sortowanie według elementów dokumentów
- Zapytania dotyczące elementów projektu z dokumentu JSON do wyników
- Aktualizowanie i zapisywanie zmian w dokumentach JSON
Istniejąca dokumentacja z nowości w programie EF7 zawiera szczegółowe informacje na temat mapowania, zapytań i aktualizacji JSON. Ta dokumentacja dotyczy teraz również programu SQLite.
Napiwek
Kod przedstawiony w dokumentacji platformy EF7 został zaktualizowany, aby można było go również uruchomić na platformie SQLite, można znaleźć w JsonColumnsSample.cs.
Zapytania do kolumn JSON
Zapytania w kolumnach JSON w bibliotece SQLite używają json_extract
funkcji . Na przykład zapytanie "autorzy w chigley" z dokumentacji, do których odwołuje się powyżej:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
W przypadku korzystania z biblioteki SQLite jest tłumaczona na następujący kod SQL:
SELECT "a"."Id", "a"."Name", "a"."Contact"
FROM "Authors" AS "a"
WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'
Aktualizowanie kolumn JSON
W przypadku aktualizacji program EF używa json_set
funkcji w bibliotece SQLite. Na przykład podczas aktualizowania pojedynczej właściwości w dokumencie:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
Program EF generuje następujące 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']
Które używają json_set
funkcji w sqlite:
UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]'))
WHERE "Id" = @p1
RETURNING 1;
HierarchyId na platformie .NET i platformie EF Core
Usługi Azure SQL i SQL Server mają specjalny typ danych, hierarchyid
który jest używany do przechowywania danych hierarchicznych. W takim przypadku "dane hierarchiczne" zasadniczo oznaczają dane, które stanowią strukturę drzewa, gdzie każdy element może mieć element nadrzędny i/lub podrzędny. Przykłady takich danych to:
- Struktura organizacyjna
- System plików
- Zestaw zadań w projekcie
- Taksonomia terminów językowych
- Wykres łączy między stronami sieci Web
Baza danych może następnie uruchamiać zapytania względem tych danych przy użyciu jego struktury hierarchicznej. Na przykład zapytanie może znaleźć elementy nadrzędne i zależne od danych elementów lub znaleźć wszystkie elementy w określonej głębokości w hierarchii.
Obsługa platform .NET i EF Core
Oficjalna obsługa typu programu SQL Server hierarchyid
jest ostatnio dostępna tylko na nowoczesnych platformach .NET (tj. ".NET Core"). Ta obsługa jest w postaci pakietu NuGet Microsoft.SqlServer.Types , który oferuje typy specyficzne dla programu SQL Server niskiego poziomu. W tym przypadku typ niskiego poziomu nosi nazwę SqlHierarchyId
.
Na następnym poziomie wprowadzono nowy pakiet Microsoft.EntityFrameworkCore.SqlServer.Abstractions , który zawiera typ wyższego poziomu HierarchyId
przeznaczony do użycia w typach jednostek.
Napiwek
Typ HierarchyId
jest bardziej idiotyczny do norm platformy .NET niż SqlHierarchyId
, który zamiast tego jest modelowany po tym, jak typy programu .NET Framework są hostowane wewnątrz aparatu bazy danych programu SQL Server. HierarchyId
jest przeznaczony do pracy z programem EF Core, ale może być również używany poza programem EF Core w innych aplikacjach. Pakiet Microsoft.EntityFrameworkCore.SqlServer.Abstractions
nie odwołuje się do żadnych innych pakietów, dlatego ma minimalny wpływ na rozmiar i zależności wdrożonej aplikacji.
Użycie funkcji HierarchyId
programu EF Core, takich jak zapytania i aktualizacje, wymaga pakietu Microsoft.EntityFrameworkCore.SqlServer.HierarchyId . Ten pakiet wprowadza zależności Microsoft.EntityFrameworkCore.SqlServer.Abstractions
Microsoft.SqlServer.Types
przechodnie i tak często jest jedynym wymaganym pakietem. Po zainstalowaniu pakietu użycie polecenia HierarchyId
jest włączone przez wywołanie w ramach wywołania UseHierarchyId
aplikacji do UseSqlServer
metody . Na przykład:
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
Uwaga
Nieoficjalna obsługa hierarchyid
programu EF Core jest dostępna od wielu lat za pośrednictwem pakietu EntityFrameworkCore.SqlServer.HierarchyId . Ten pakiet został zachowany jako współpraca między społecznością a zespołem EF. Teraz, gdy istnieje oficjalna obsługa platformy hierarchyid
.NET, kod z tych formularzy pakietów społeczności, z uprawnieniem oryginalnych współautorów, podstawy oficjalnego pakietu opisanego tutaj. Wiele dzięki wszystkim zaangażowanym osobom z lat, w tym @aljones, @cutig3r, @huan086, @kmataru, @mehdihaghshenas i @vyrotek
Hierarchie modelowania
Typ HierarchyId
może służyć do właściwości typu jednostki. Załóżmy na przykład, że chcemy modelować drzewo rodzinne ojcowskie niektórych fikcyjnych półlingów. W typie jednostki dla Halfling
HierarchyId
elementu właściwość może służyć do lokalizowania każdego półlinga w drzewie rodzinnym.
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; }
}
Napiwek
Kod przedstawiony tutaj i w poniższych przykładach pochodzi z HierarchyIdSample.cs.
Napiwek
W razie potrzeby HierarchyId
nadaje się do użycia jako typ właściwości klucza.
W tym przypadku drzewo rodzinne jest zakorzenione patriarchą rodziny. Każdy półling można prześledzić od patriarchy w dół drzewa przy użyciu jego PathFromPatriarch
właściwości. Program SQL Server używa kompaktowego formatu binarnego dla tych ścieżek, ale często analizowanie do i z reprezentacji ciągu czytelnego dla człowieka podczas pracy z kodem. W tej reprezentacji pozycja na każdym poziomie jest oddzielona znakiem /
. Rozważmy na przykład drzewo rodzinne na poniższym diagramie:
W tym drzewie:
- Balbo znajduje się u korzenia drzewa reprezentowanego przez
/
element . - Balbo ma pięcioro dzieci reprezentowane przez
/1/
, ,/3/
/2/
,/4/
i/5/
. - Pierwsze dziecko Balbo, Mungo, ma również pięcioro dzieci reprezentowane przez
/1/1/
, ,/1/2/
/1/3/
,/1/4/
i/1/5/
. Zwróć uwagę, że dlaHierarchyId
Balbo (/1/
) jest prefiksem dla wszystkich jego dzieci. - Podobnie trzecie dziecko Balbo, Ponto, ma dwoje dzieci, reprezentowane przez
/3/1/
i/3/2/
. Ponownie każdy z tych elementów podrzędnych jest poprzedzony prefiksemHierarchyId
dla Ponto, który jest reprezentowany jako/3/
. - I tak dalej w dół drzewa...
Poniższy kod wstawia to drzewo rodziny do bazy danych przy użyciu programu 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();
Napiwek
W razie potrzeby wartości dziesiętne mogą służyć do tworzenia nowych węzłów między dwoma istniejącymi węzłami. Na przykład /3/2.5/2/
przechodzi między /3/2/2/
i /3/3/2/
.
Wykonywanie zapytań dotyczących hierarchii
HierarchyId
Uwidacznia kilka metod, które mogą być używane w zapytaniach LINQ.
Metoda | opis |
---|---|
GetAncestor(int n) |
Pobiera poziomy węzłów n w górę drzewa hierarchicznego. |
GetDescendant(HierarchyId? child1, HierarchyId? child2) |
Pobiera wartość węzła podrzędnego, który jest większy niż child1 i mniejszy niż child2 . |
GetLevel() |
Pobiera poziom tego węzła w drzewie hierarchicznym. |
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) |
Pobiera wartość reprezentującą lokalizację nowego węzła, który ma ścieżkę z równej ścieżce od newRoot oldRoot do tej, skutecznie przenosząc tę wartość do nowej lokalizacji. |
IsDescendantOf(HierarchyId? parent) |
Pobiera wartość wskazującą, czy ten węzeł jest elementem podrzędnym parent . |
Ponadto można użyć operatorów ==
, !=
, <
<=
, >
i >=
.
Poniżej przedstawiono przykłady użycia tych metod w zapytaniach LINQ.
Pobieranie jednostek na danym poziomie w drzewie
Następujące zapytanie używa GetLevel
metody , aby zwrócić wszystkie półlingi na danym poziomie w drzewie rodziny:
var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
Przekłada się to na następujący kod SQL:
SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
Uruchomienie tego w pętli pozwala uzyskać półlingi dla każdej generacji:
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
Pobieranie bezpośredniego przodka jednostki
Następujące zapytanie używa GetAncestor
metody do znalezienia bezpośredniego przodka półlinga, biorąc pod uwagę nazwę halflinga:
async Task<Halfling?> FindDirectAncestor(string name)
=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name == name).PathFromPatriarch
.GetAncestor(1));
Przekłada się to na następujący kod 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)
Uruchomienie tego zapytania dla półlingu "Bilbo" zwraca wartość "Bungo".
Pobieranie bezpośrednich malejących jednostki
Następujące zapytanie używa również elementu GetAncestor
, ale tym razem w celu znalezienia bezpośrednich malejących półlinga, biorąc pod uwagę nazwę halflinga:
IQueryable<Halfling> FindDirectDescendents(string name)
=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);
Przekłada się to na następujący kod 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)
Uruchomienie tego zapytania dla półlingu "Mungo" zwraca wartości "Bungo", "Belba", "Longo" i "Linda".
Pobieranie wszystkich elementów podrzędnych jednostki
GetAncestor
jest przydatna do wyszukiwania w górę lub w dół pojedynczego poziomu, a nawet określonej liczby poziomów. Z drugiej strony jest IsDescendantOf
przydatna do znajdowania wszystkich przodków lub zależności. Na przykład następujące zapytanie używa IsDescendantOf
metody , aby znaleźć wszystkie elementy podrzędne półlinga, biorąc pod uwagę nazwę halflinga:
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());
Ważne
IsDescendantOf
zwraca wartość true dla siebie, dlatego jest on filtrowany w powyższym zapytaniu.
Przekłada się to na następujący kod 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
Uruchomienie tego zapytania dla półlingu "Bilbo" zwraca wartości "Bungo", "Mungo" i "Balbo".
Pobieranie wszystkich malejących jednostek
Następujące zapytanie używa również elementu IsDescendantOf
, ale tym razem do wszystkich malejących półlinga, biorąc pod uwagę nazwę halflinga:
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());
Przekłada się to na następujący kod 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()
Uruchomienie tego zapytania dla półlingu "Mungo" zwraca "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho" i "Poppy".
Znajdowanie wspólnego przodka
Jednym z najczęstszych pytań zadawanych na temat tego konkretnego drzewa rodzinnego jest "kto jest wspólnym przodkiem Frodo i Bilbo?" Możemy użyć IsDescendantOf
polecenia , aby napisać takie zapytanie:
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();
Przekłada się to na następujący kod 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
Uruchomienie tego zapytania z "Bilbo" i "Frodo" informuje nas, że ich wspólny przodk jest "Balbo".
Aktualizowanie hierarchii
Do aktualizowania hierarchyid
kolumn można używać mechanizmów śledzenia normalnych zmian i funkcji SaveChanges.
Ponowne nadrzędne pod hierarchię
Na przykład, jestem pewien, że wszyscy pamiętamy skandal SR 1752 (np. "LongoGate"), gdy badania DNA wykazały, że Longo nie był w rzeczywistości synem Mungo, ale rzeczywiście synem Ponto! Jednym z opadów z tego skandalu było to, że drzewo rodzinne musi być ponownie napisane. W szczególności Longo i wszystkie jego zstępstwa musiały zostać ponownie wychowane z Mungo do Ponto. GetReparentedValue
może służyć do tego celu. Na przykład najpierw "Longo" i wszystkie jego zstępne są odpytywane:
var longoAndDescendents = await context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
.ToListAsync();
Następnie GetReparentedValue
służy do aktualizowania HierarchyId
elementu dla longo i każdego malejącej, a następnie wywołania metody :SaveChangesAsync
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
Spowoduje to następującą aktualizację bazy danych:
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;
Przy użyciu następujących parametrów:
@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)
Uwaga
Wartości parametrów właściwości HierarchyId
są wysyłane do bazy danych w kompaktowym formacie binarnym.
Po aktualizacji zapytanie dotyczące malejących wartości "Mungo" zwraca wartość "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco" i "Poppy", podczas wykonywania zapytań o malejące wartości "Ponto", "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony" i "Angelica".
Nieprzetworzone zapytania SQL dla niemapowanych typów
Program EF7 wprowadził nieprzetworzone zapytania SQL zwracające typy skalarne. Jest to ulepszone w programie EF8 w celu uwzględnienia nieprzetworzonych zapytań SQL zwracających dowolny typ mapowalnego środowiska CLR bez uwzględniania tego typu w modelu EF.
Napiwek
Pokazany tutaj kod pochodzi z RawSqlSample.cs.
Zapytania korzystające z niemapowanych typów są wykonywane przy użyciu polecenia SqlQuery lub SqlQueryRaw. Pierwszy używa interpolacji ciągów do sparametryzowania zapytania, co pomaga upewnić się, że wszystkie wartości inne niż stałe są sparametryzowane. Rozważmy na przykład następującą tabelę bazy danych:
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
Może służyć do wykonywania zapytań względem tej tabeli i zwracania wystąpień BlogPost
typu z właściwościami odpowiadającymi kolumnom w tabeli:
Na przykład:
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; }
}
Na przykład:
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();
To zapytanie jest sparametryzowane i wykonywane jako:
SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1
Typ używany dla wyników zapytania może zawierać typowe konstrukcje mapowania obsługiwane przez program EF Core, takie jak konstruktory sparametryzowane i atrybuty mapowania. Na przykład:
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; }
}
Uwaga
Typy używane w ten sposób nie mają zdefiniowanych kluczy i nie mogą mieć relacji z innymi typami. Typy z relacjami muszą być mapowane w modelu.
Użyty typ musi mieć właściwość dla każdej wartości w zestawie wyników, ale nie musi odpowiadać żadnej tabeli w bazie danych. Na przykład następujący typ reprezentuje tylko podzbiór informacji dla każdego wpisu i zawiera nazwę bloga, która pochodzi z Blogs
tabeli:
public class PostSummary
{
public string BlogName { get; set; } = null!;
public string PostTitle { get; set; } = null!;
public DateOnly? PublishedOn { get; set; }
}
Zapytania można wykonywać przy użyciu SqlQuery
w taki sam sposób jak poprzednio:
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();
Jedną z miłych SqlQuery
cech jest to, że zwraca element IQueryable
, który można skomponować przy użyciu LINQ. Na przykład do powyższego zapytania można dodać klauzulę "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();
Jest to wykonywane jako:
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
W tym momencie warto pamiętać, że wszystkie powyższe czynności można wykonać całkowicie w LINQ bez konieczności pisania jakiegokolwiek kodu SQL. Obejmuje to zwracanie wystąpień niezamapowanego typu, takiego jak PostSummary
. Na przykład powyższe zapytanie można napisać w 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();
Co przekłada się na znacznie bardziej czystszy 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
Napiwek
Program EF jest w stanie wygenerować czystszy program SQL, gdy jest odpowiedzialny za całe zapytanie, niż podczas komponowania w języku SQL dostarczonym przez użytkownika, ponieważ w poprzednim przypadku pełna semantyka zapytania jest dostępna dla platformy EF.
Do tej pory wszystkie zapytania zostały wykonane bezpośrednio względem tabel. SqlQuery
Może również służyć do zwracania wyników z widoku bez mapowania typu widoku w modelu EF. Na przykład:
var summariesFromView =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM PostAndBlogSummariesView")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
SqlQuery
Podobnie można użyć do wyników funkcji:
var summariesFromFunc =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
.Where(p => p.PublishedOn < end)
.ToListAsync();
Zwrócony IQueryable
element może składać się, gdy jest wynikiem widoku lub funkcji, podobnie jak w przypadku wyniku zapytania tabeli. Procedury składowane można również wykonywać przy użyciu metody SqlQuery
, ale większość baz danych nie obsługuje ich tworzenia. Na przykład:
var summariesFromStoredProc =
await context.Database.SqlQuery<PostSummary>(
@$"exec GetRecentPostSummariesProc")
.ToListAsync();
Ulepszenia ładowania z opóźnieniem
Ładowanie z opóźnieniem dla zapytań bez śledzenia
Program EF8 dodaje obsługę opóźnionego ładowania nawigacji na jednostkach , które nie są śledzone przez element DbContext
. Oznacza to, że zapytanie bez śledzenia może być wykonywane przez leniwe ładowanie nawigacji na jednostkach zwracanych przez zapytanie bez śledzenia.
Napiwek
Kod przykładów ładowania z opóźnieniem pokazanych poniżej pochodzi z LazyLoadingSample.cs.
Rozważmy na przykład zapytanie bez śledzenia dla blogów:
var blogs = await context.Blogs.AsNoTracking().ToListAsync();
Jeśli Blog.Posts
skonfigurowano ładowanie z opóźnieniem, na przykład przy użyciu serwerów proxy ładujących z opóźnieniem, uzyskanie dostępu spowoduje załadowanie Posts
z bazy danych:
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}");
}
}
Program EF8 zgłasza również, czy dana nawigacja jest ładowana dla jednostek, które nie są śledzone przez kontekst. Na przykład:
foreach (var blog in blogs)
{
if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
{
Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
}
}
Podczas korzystania z ładowania leniwego w ten sposób należy wziąć pod uwagę kilka ważnych kwestii:
- Ładowanie z opóźnieniem powiedzie się tylko do momentu
DbContext
usunięcia jednostki użytej do wykonywania zapytań. - Jednostki, do których są odpytywane w ten sposób, zachowują odwołanie do elementu
DbContext
, mimo że nie są śledzone przez nie. Należy zachować ostrożność, aby uniknąć przecieków pamięci, jeśli wystąpienia jednostek będą miały długie okresy istnienia. - Jawne odłączenie jednostki przez ustawienie jej stanu na
EntityState.Detached
zerwanie odwołania doDbContext
i leniwe ładowanie nie będzie już działać. - Należy pamiętać, że wszystkie leniwe operacje ładowania używają synchronicznych operacji we/wy, ponieważ nie ma możliwości uzyskania dostępu do właściwości w sposób asynchroniczny.
Ładowanie z nieśledzonych jednostek działa zarówno dla serwerów proxy z opóźnieniem ładowania, jak i ładowania z opóźnieniem bez serwerów proxy.
Jawne ładowanie z nieśledzonych jednostek
Program EF8 obsługuje ładowanie nawigacji na nieśledzonych jednostkach nawet wtedy, gdy jednostka lub nawigacja nie jest skonfigurowana do ładowania z opóźnieniem. W przeciwieństwie do ładowania leniwego, to jawne ładowanie można wykonać asynchronicznie. Na przykład:
await context.Entry(blog).Collection(e => e.Posts).LoadAsync();
Rezygnacja z opóźnionego ładowania dla określonych nawigacji
Program EF8 umożliwia konfigurację określonych nawigacji, które nie są ładowane z opóźnieniem, nawet jeśli wszystko inne jest skonfigurowane w taki sposób. Na przykład aby skonfigurować nawigację Post.Author
tak, aby nie ładowała się z opóźnieniem, wykonaj następujące czynności:
modelBuilder
.Entity<Post>()
.Navigation(p => p.Author)
.EnableLazyLoading(false);
Wyłączenie ładowania z opóźnieniem, tak jak to działa w przypadku serwerów proxy ładowanych z opóźnieniem i ładowania z opóźnieniem bez serwerów proxy.
Ładowanie serwerów proxy z opóźnieniem działa przez zastąpienie właściwości nawigacji wirtualnej. W klasycznych aplikacjach EF6 typowe źródło usterek zapomina o utworzeniu nawigacji wirtualnej, ponieważ nawigacja w trybie dyskretnym nie będzie ładowana z opóźnieniem. W związku z tym serwery proxy ef Core są domyślnie zgłaszane, gdy nawigacja nie jest wirtualna.
Można to zmienić w programie EF8, aby wyrazić zgodę na klasyczne zachowanie ef6, tak aby nawigacja nie ładowała się z opóźnieniem, przez co nawigacja nie jest wirtualna. Ta zgoda jest konfigurowana jako część wywołania metody UseLazyLoadingProxies
. Na przykład:
optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());
Dostęp do śledzonych jednostek
Wyszukiwanie śledzonych jednostek według klucza podstawowego, alternatywnego lub obcego
Wewnętrznie platforma EF obsługuje struktury danych do znajdowania śledzonych jednostek według klucza podstawowego, alternatywnego lub obcego. Te struktury danych są używane do wydajnego naprawiania między powiązanymi jednostkami, gdy nowe jednostki są śledzone lub zmieniają się relacje.
Program EF8 zawiera nowe publiczne interfejsy API, dzięki czemu aplikacje mogą teraz używać tych struktur danych do wydajnego wyszukiwania śledzonych jednostek. Te interfejsy API są dostępne za pośrednictwem LocalView<TEntity> typu jednostki. Aby na przykład wyszukać śledzonej jednostki według klucza podstawowego:
var blogEntry = context.Blogs.Local.FindEntry(2)!;
Napiwek
Pokazany tutaj kod pochodzi z LookupByKeySample.cs.
Metoda FindEntry
zwraca wartość EntityEntry<TEntity> dla śledzonej jednostki lub null
jeśli nie jest śledzona żadna jednostka z danym kluczem. Podobnie jak wszystkie metody w systemie LocalView
, baza danych nigdy nie jest odpytywane, nawet jeśli jednostka nie zostanie znaleziona. Zwrócony wpis zawiera samą jednostkę, a także informacje o śledzeniu. Na przykład:
Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key {blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");
Wyszukiwanie jednostki za pomocą dowolnego elementu innego niż klucz podstawowy wymaga określenia nazwy właściwości. Aby na przykład wyszukać klucz alternatywny:
var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new Uri("https://www.bricelam.net/"))!;
Możesz też wyszukać unikatowy klucz obcy:
var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri), new Uri("https://www.bricelam.net/"))!;
Do tej pory wyszukiwania zawsze zwracały pojedynczy wpis lub null
. Jednak niektóre wyszukiwania mogą zwracać więcej niż jeden wpis, na przykład podczas wyszukiwania za pomocą klucza obcego, który nie jest unikatowy. Metoda GetEntries
powinna być używana dla tych odnośników. Na przykład:
var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);
We wszystkich tych przypadkach wartość używana do wyszukiwania jest kluczem podstawowym, kluczem alternatywnym lub wartością klucza obcego. Program EF używa wewnętrznych struktur danych dla tych odnośników. Jednak wyszukiwania według wartości mogą być również używane dla wartości dowolnej właściwości lub kombinacji właściwości. Aby na przykład znaleźć wszystkie zarchiwizowane wpisy:
var archivedPostEntries = context.Posts.Local.GetEntries(nameof(Post.Archived), true);
To wyszukiwanie wymaga skanowania wszystkich śledzonych Post
wystąpień i dlatego będzie mniej wydajne niż wyszukiwanie kluczy. Jednak zwykle jest to nadal szybsze niż naiwne zapytania przy użyciu polecenia ChangeTracker.Entries<TEntity>().
Na koniec można również wykonywać wyszukiwania względem kluczy złożonych, innych kombinacji wielu właściwości lub gdy typ właściwości nie jest znany w czasie kompilacji. Na przykład:
var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new object[] { 4, "TagEF" });
Kompilowanie modelu
Kolumny dyskryminujące mają maksymalną długość
W programie EF8 kolumny dyskryminujące ciągów używane do mapowania dziedziczenia TPH są teraz skonfigurowane z maksymalną długością. Ta długość jest obliczana jako najmniejsza liczba Fibonacciego, która obejmuje wszystkie zdefiniowane wartości dyskryminujące. Rozważmy na przykład następującą hierarchię:
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; }
}
Zgodnie z konwencją używania nazw klas dla wartości dyskryminujących możliwe wartości to "PaperbackEdition", "HardbackEdition" i "Magazine", a tym samym kolumna dyskryminująca jest skonfigurowana dla maksymalnej długości 21. Na przykład w przypadku korzystania z programu SQL Server:
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]),
Napiwek
Liczby Fibonacciego służą do ograniczania liczby wygenerowania migracji w celu zmiany długości kolumny w miarę dodawania nowych typów do hierarchii.
Funkcja DateOnly/TimeOnly obsługiwana w programie SQL Server
Typy DateOnly i TimeOnly zostały wprowadzone na platformie .NET 6 i są obsługiwane dla kilku dostawców baz danych (np. SQLite, MySQL i PostgreSQL) od czasu ich wprowadzenia. W przypadku programu SQL Server najnowsza wersja pakietu Microsoft.Data.SqlClient przeznaczonego dla platformy .NET 6 umożliwiła ErikEJ dodanie obsługi tych typów na poziomie ADO.NET. To z kolei utorowało drogę do obsługi w programie EF8 dla DateOnly
i TimeOnly
jako właściwości w typach jednostek.
Napiwek
DateOnly
można ich TimeOnly
używać w programach EF Core 6 i 7 przy użyciu pakietu społecznościowego ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnlyOnly z @ErikEJ.
Rozważmy na przykład następujący model EF dla szkół brytyjskich:
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; }
}
Napiwek
Pokazany tutaj kod pochodzi z DateOnlyTimeOnlySample.cs.
Uwaga
Ten model reprezentuje tylko brytyjskie szkoły i przechowuje czasy w czasie lokalnym (GMT). Obsługa różnych stref czasowych znacznie komplikuje ten kod. Należy pamiętać, że użycie DateTimeOffset
metody nie pomogłoby w tym miejscu, ponieważ czas otwierania i zamykania ma różne przesunięcia w zależności od tego, czy czas letni jest aktywny, czy nie.
Te typy jednostek są mapowe na poniższe tabele podczas korzystania z programu SQL Server. Zwróć uwagę, że DateOnly
właściwości są mapowania na date
kolumny i TimeOnly
właściwości mapowania na time
kolumny.
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);
Zapytania korzystające z funkcji DateOnly
i TimeOnly
działają w oczekiwany sposób. Na przykład następujące zapytanie LINQ znajduje szkoły, które są obecnie otwarte:
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();
To zapytanie przekłada się na następujący kod SQL, jak pokazano w ToQueryStringpliku :
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
można TimeOnly
również używać w kolumnach JSON. Na przykład OpeningHours
można zapisać jako dokument JSON, co spowoduje, że dane wyglądają następująco:
Kolumna | Wartość |
---|---|
Id | 2 |
Nazwisko | Farr High School |
Założona | 1964-05-01 |
OtwieranieHours | [ |
Połączenie dwóch funkcji z platformy EF8 umożliwia teraz wykonywanie zapytań o godziny otwarcia przez indeksowanie w kolekcji JSON. Na przykład:
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();
To zapytanie przekłada się na następujący kod SQL, jak pokazano w ToQueryStringpliku :
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
Na koniec aktualizacje i usunięcia można wykonać za pomocą funkcji śledzenia i zapisywania zmian lub za pomocą polecenia ExecuteUpdate/ExecuteDelete. Na przykład:
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)));
Ta aktualizacja przekłada się na następujący kod 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)
Inżynier odwrotny synapse i usługa Dynamics 365 TDS
Inżynieria odwrotna EF8 (czyli tworzenie szkieletów z istniejącej bazy danych) obsługuje teraz bezserwerową pulę SQL usługi Synapse i bazy danych punktów końcowych TDS usługi Dynamics 365.
Ostrzeżenie
Te systemy baz danych różnią się od normalnych baz danych SQL Server i Azure SQL Database. Te różnice oznaczają, że nie wszystkie funkcje platformy EF Core są obsługiwane podczas pisania zapytań względem tych systemów baz danych lub wykonywania innych operacji.
Ulepszenia tłumaczeń matematycznych
Ogólne interfejsy matematyczne zostały wprowadzone na platformie .NET 7. Konkretne typy, takie jak double
i zaimplementowane, dodają nowe interfejsy API dublujące istniejące funkcje matematyki i matematykifloat
.
Program EF Core 8 tłumaczy wywołania tych ogólnych interfejsów API matematycznych w LINQ przy użyciu istniejących tłumaczeń SQL dostawców dla i Math
MathF
. Oznacza to, że możesz wybrać między wywołaniami takimi jak Math.Sin
lub double.Sin
w zapytaniach EF.
We współpracy z zespołem platformy .NET dodaliśmy dwie nowe ogólne metody matematyczne na platformie .NET 8, które są implementowane na platformie double
i float
. Są one również tłumaczone na język SQL w programie EF Core 8.
.NET | SQL |
---|---|
DegreesToRadians | RADIANS |
RadianyToDegrees | DEGREES |
Na koniec pracowaliśmy z Eric Sink w projekcie SQLitePCLRaw, aby umożliwić funkcji matematycznych SQLite w swoich kompilacjach natywnej biblioteki SQLite. Obejmuje to bibliotekę natywną uzyskaną domyślnie podczas instalowania dostawcy EF Core SQLite. Umożliwia to kilka nowych tłumaczeń SQL w LINQ, w tym: Acos, Acosh, Asin, Asinh, Atan, Atan, Atan2, Atanh, Ceiling, Cos, Cosh, DegreesToRadians, Exp, Floor, Log2, Log10, Pow, RadiansToDegrees, Sign, Sinh, Sqrt, Tanh i Truncate.
Sprawdzanie oczekujących zmian modelu
Dodaliśmy nowe dotnet ef
polecenie, aby sprawdzić, czy jakiekolwiek zmiany modelu zostały wprowadzone od ostatniej migracji. Może to być przydatne w scenariuszach ciągłej integracji/ciągłego wdrażania, aby upewnić się, że użytkownik lub kolega z zespołu nie zapomnieli dodać migracji.
dotnet ef migrations has-pending-model-changes
Możesz również wykonać to sprawdzanie programowo w aplikacji lub testach przy użyciu nowej dbContext.Database.HasPendingModelChanges()
metody.
Ulepszenia tworzenia szkieletów SQLite
SqLite obsługuje tylko cztery typy danych pierwotnych — INTEGER, REAL, TEXT i BLOB. Wcześniej oznaczało to, że w przypadku odwrotnego utworzenia bazy danych SQLite do utworzenia szkieletu modelu EF Core wynikowe typy jednostek obejmowały tylko właściwości typu long
, , double
string
i byte[]
. Dodatkowe typy platformy .NET są obsługiwane przez dostawcę EF Core SQLite przez konwertowanie między nimi a jednym z czterech pierwotnych typów SQLite.
W programie EF Core 8 używamy teraz nazwy formatu danych i typu kolumny oprócz typu SQLite w celu określenia bardziej odpowiedniego typu platformy .NET do użycia w modelu. W poniższych tabelach przedstawiono niektóre przypadki, w których dodatkowe informacje prowadzą do lepszych typów właściwości w modelu.
Nazwa typu kolumny | Typ platformy .NET |
---|---|
BOOLOWSKI | |
SMALLINT | |
INT | |
BIGINT | długi |
STRUNA |
Format danych | Typ platformy .NET |
---|---|
'0.0' | |
'1970-01-01' | |
'1970-01-01 00:00:00' | |
'00:00:00' | |
'00000000-0000-0000-0000-000000000000' |
Wartości usługi Sentinel i wartości domyślne bazy danych
Bazy danych umożliwiają skonfigurowanie kolumn w celu wygenerowania wartości domyślnej, jeśli podczas wstawiania wiersza nie podano żadnej wartości. Można to przedstawić w programie EF przy użyciu HasDefaultValue
dla stałych:
b.Property(e => e.Status).HasDefaultValue("Hidden");
Lub HasDefaultValueSql
w przypadku dowolnych klauzul SQL:
b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");
Napiwek
Poniższy kod pochodzi z DefaultConstraintSample.cs.
Aby program EF korzystał z tej funkcji, musi określić, kiedy i kiedy nie wysyłać wartości dla kolumny. Domyślnie program EF używa domyślnego ustawienia CLR jako sentinel. Oznacza to, że gdy wartość Status
lub LeaseDate
w powyższych przykładach są wartościami domyślnymi CLR dla tych typów, program EF interpretuje, że oznacza to, że właściwość nie została ustawiona, a więc nie wysyła wartości do bazy danych. Działa to dobrze w przypadku typów referencyjnych — na przykład jeśli właściwość to , program EF nie wysyła null
do bazy danych, ale raczej nie zawiera żadnej wartości, tak aby używana była domyślna baza danych ("Hidden"
).null
Status
string
Podobnie dla DateTime
właściwości LeaseDate
program EF nie wstawi wartości domyślnej 1/1/0001 12:00:00 AM
CLR , ale zamiast tego pominą tę wartość, tak aby używana była wartość domyślna bazy danych.
Jednak w niektórych przypadkach wartość domyślna CLR jest prawidłową wartością do wstawienia. Program EF8 obsługuje to, zezwalając na zmianę wartości sentinel dla kolumny. Rozważmy na przykład kolumnę całkowitą skonfigurowaną z wartością domyślną bazy danych:
b.Property(e => e.Credits).HasDefaultValueSql(10);
W takim przypadku chcemy, aby nowa jednostka została wstawiona z daną liczbą środków, chyba że nie zostanie określona, w takim przypadku zostanie przypisanych 10 kredytów. Oznacza to jednak, że wstawianie rekordu z zerowymi środkami nie jest możliwe, ponieważ zero jest wartością domyślną CLR, co spowoduje, że program EF nie wyśle żadnej wartości. W programie EF8 można to naprawić, zmieniając wartość sentinel dla właściwości z zera na -1
:
b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);
Program EF będzie teraz używać wartości domyślnej bazy danych tylko wtedy, gdy Credits
jest ustawiona na -1
wartość ; wartość zero zostanie wstawiona jak każda inna kwota.
Często warto to odzwierciedlić w typie jednostki, a także w konfiguracji ef. Na przykład:
public class Person
{
public int Id { get; set; }
public int Credits { get; set; } = -1;
}
Oznacza to, że wartość sentinel -1 jest ustawiana automatycznie po utworzeniu wystąpienia, co oznacza, że właściwość rozpoczyna się w stanie "not-set".
Napiwek
Jeśli chcesz skonfigurować domyślne ograniczenie bazy danych do użycia podczas Migrations
tworzenia kolumny, ale chcesz, aby program EF zawsze wstawił wartość, skonfiguruj właściwość jako niegenerowaną. Na przykład b.Property(e => e.Credits).HasDefaultValueSql(10).ValueGeneratedNever();
.
Wartości domyślne bazy danych dla wartości logicznych
Właściwości logiczne stanowią skrajną formę tego problemu, ponieważ wartość domyślna CLR (false
) jest jedną z dwóch prawidłowych wartości. Oznacza to, że bool
właściwość z domyślnym ograniczeniem bazy danych będzie miała wstawioną wartość tylko wtedy, gdy ta wartość to true
. Gdy wartość domyślna bazy danych to false
, oznacza to, że gdy wartość właściwości to false
, zostanie użyta wartość domyślna bazy danych, czyli false
. W przeciwnym razie, jeśli wartość właściwości to true
, true
zostanie wstawiona. Dlatego gdy wartość domyślna bazy danych to false
, kolumna bazy danych kończy się poprawną wartością.
Z drugiej strony, jeśli wartość domyślna bazy danych to true
, oznacza to, że gdy wartość właściwości to false
, zostanie użyta wartość domyślna bazy danych, czyli true
! A gdy wartość właściwości to true
, true
zostanie wstawiona. Dlatego wartość w kolumnie zawsze kończy się true
w bazie danych, niezależnie od tego, jaka jest wartość właściwości.
Program EF8 rozwiązuje ten problem, ustawiając sentinel dla właściwości logicznych na taką samą wartość jak wartość domyślna bazy danych. Oba powyższe przypadki powodują wstawienie poprawnej wartości niezależnie od tego, czy domyślna true
jest baza danych, czy false
.
Napiwek
Podczas tworzenia szkieletu z istniejącej bazy danych program EF8 analizuje i dołącza proste wartości domyślne do HasDefaultValue
wywołań. (Wcześniej wszystkie wartości domyślne były szkieletowe jako nieprzezroczyste HasDefaultValueSql
wywołania). Oznacza to, że kolumny logiczne bez wartości null z wartością domyślną lub false
stałą bazą true
danych nie są już szkieletowe jako dopuszczające wartość null.
Wartości domyślne bazy danych dla wyliczenia
Właściwości wyliczenia mogą mieć podobne problemy z bool
właściwościami, ponieważ wyliczenia zwykle mają bardzo mały zestaw prawidłowych wartości, a wartość domyślna CLR może być jedną z tych wartości. Rozważmy na przykład ten typ jednostki i wyliczenie:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; }
}
public enum Level
{
Beginner,
Intermediate,
Advanced,
Unspecified
}
Właściwość Level
jest następnie konfigurowana z wartością domyślną bazy danych:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate);
W przypadku tej konfiguracji program EF wykluczy wysyłanie wartości do bazy danych, gdy jest ona ustawiona na Level.Beginner
, a zamiast tego Level.Intermediate
jest przypisywana przez bazę danych. To nie jest to, co było zamierzone!
Problem nie miałby miejsca, gdyby wyliczenie zostało zdefiniowane z wartością "nieznaną" lub "nieokreśloną" jako domyślną bazą danych:
public enum Level
{
Unspecified,
Beginner,
Intermediate,
Advanced
}
Jednak nie zawsze można zmienić istniejące wyliczenie, więc w ef8 można ponownie określić sentinel. Na przykład powrót do oryginalnego wyliczenia:
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate)
.HasSentinel(Level.Unspecified);
Teraz Level.Beginner
zostanie wstawiona normalnie, a wartość domyślna bazy danych będzie używana tylko wtedy, gdy wartość właściwości to Level.Unspecified
. Może to być przydatne, aby odzwierciedlić to w samym typie jednostki. Na przykład:
public class Course
{
public int Id { get; set; }
public Level Level { get; set; } = Level.Unspecified;
}
Używanie pola kopii zapasowej dopuszczającej wartość null
Bardziej ogólnym sposobem obsługi opisanego powyżej problemu jest utworzenie pola kopii zapasowej dopuszczającej wartość null dla właściwości bez wartości null. Rozważmy na przykład następujący typ jednostki z właściwością bool
:
public class Account
{
public int Id { get; set; }
public bool IsActive { get; set; }
}
Właściwość może mieć pole kopii zapasowej dopuszczającej wartość null:
public class Account
{
public int Id { get; set; }
private bool? _isActive;
public bool IsActive
{
get => _isActive ?? false;
set => _isActive = value;
}
}
Pole zapasowe pozostanie w tym miejscu null
, chyba że zestaw właściwości zostanie rzeczywiście wywołany. Oznacza to, że wartość pola zapasowego jest lepszym wskazaniem, czy właściwość została ustawiona, czy nie niż wartość domyślna CLR właściwości. To działa poza polem z ef, ponieważ ef będzie używać pola zapasowego do odczytywania i zapisywania właściwości domyślnie.
Better ExecuteUpdate i ExecuteDelete
Polecenia SQL, które wykonują aktualizacje i usuwanie, takie jak te generowane przez ExecuteUpdate
metody i ExecuteDelete
, muszą być przeznaczone dla pojedynczej tabeli bazy danych. Jednak w programie EF7 ExecuteUpdate
i ExecuteDelete
nie obsługiwał aktualizacji uzyskiwania dostępu do wielu typów jednostek nawet wtedy, gdy zapytanie ostatecznie wpłynęło na jedną tabelę. Program EF8 usuwa to ograniczenie. Rozważmy na przykład typ jednostki z CustomerInfo
typem Customer
własności:
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 typy jednostek są mapowania na tabelę Customers
. Jednak następująca aktualizacja zbiorcza kończy się niepowodzeniem w programie EF7, ponieważ używa obu typów jednostek:
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"));
W programie EF8 ta funkcja przekłada się teraz na następujący kod SQL podczas korzystania z usługi Azure SQL:
UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
[c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0
Podobnie wystąpienia zwracane z Union
zapytania mogą być aktualizowane tak długo, jak wszystkie aktualizacje są przeznaczone dla tej samej tabeli. Na przykład możemy zaktualizować dowolny Customer
element z regionem France
, a jednocześnie dowolnym Customer
, który odwiedził sklep z regionem 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"));
W programie EF8 to zapytanie generuje następujące informacje podczas korzystania z usługi Azure SQL:
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]
W ostatnim przykładzie w programie EF8 ExecuteUpdate
można użyć do aktualizowania jednostek w hierarchii TPT, o ile wszystkie zaktualizowane właściwości są mapowane na tę samą tabelę. Rozważmy na przykład te typy jednostek mapowane przy użyciu 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; }
}
Za pomocą programu EF8 Note
można zaktualizować właściwość:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));
Name
Można również zaktualizować właściwość:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Jednak program EF8 nie próbuje zaktualizować właściwości Name
i Note
, ponieważ są one mapowane na różne tabele. Na przykład:
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted")
.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
Zgłasza następujący wyjątek:
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.
Lepsze wykorzystanie zapytań IN
Contains
Gdy operator LINQ jest używany z podzapytaniem, platforma EF Core generuje teraz lepsze zapytania przy użyciu języka SQL IN
zamiast EXISTS
; oprócz tworzenia bardziej czytelnego kodu SQL, w niektórych przypadkach może to spowodować znacznie szybsze wykonywanie zapytań. Rozważmy na przykład następujące zapytanie LINQ:
var blogsWithPosts = await context.Blogs
.Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
.ToListAsync();
Program EF7 generuje następujące elementy dla bazy danych PostgreSQL:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE EXISTS (
SELECT 1
FROM "Posts" AS p
WHERE p."BlogId" = b."Id")
Ponieważ podzapytywanie odwołuje się do tabeli zewnętrznej Blogs
(za pośrednictwem b."Id"
), jest to skorelowana podzapytywanie, co oznacza, że Posts
podzapytywanie musi być wykonywane dla każdego wiersza w Blogs
tabeli. W programie EF8 zamiast tego jest generowany następujący kod SQL:
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE b."Id" IN (
SELECT p."BlogId"
FROM "Posts" AS p
)
Ponieważ podzapytywanie Blogs
nie odwołuje się już do elementu , można je ocenić raz, co daje ogromne ulepszenia wydajności w większości systemów baz danych. Jednak niektóre systemy baz danych, zwłaszcza sql Server, baza danych jest w stanie zoptymalizować pierwsze zapytanie do drugiego zapytania, aby wydajność była taka sama.
Numeryczne konwersje wierszy dla Usługi SQL Azure/programu SQL Server
Automatyczna optymistyczna współbieżność programu SQL Server jest obsługiwana przy użyciu rowversion
kolumn. A rowversion
to nieprzezroczysta wartość 8-bajtowa przekazywana między bazą danych, klientem i serwerem. Domyślnie program SqlClient uwidacznia rowversion
typy jako byte[]
, mimo że modyfikowalne typy odwołań są złym dopasowaniem semantyki rowversion
. W programie EF8 można łatwo mapować rowversion
kolumny na long
lub ulong
właściwości. Na przykład:
modelBuilder.Entity<Blog>()
.Property(e => e.RowVersion)
.IsRowVersion();
Eliminacja nawiasów
Generowanie czytelnego kodu SQL jest ważnym celem dla platformy EF Core. W programie EF8 wygenerowany język SQL jest bardziej czytelny dzięki automatycznej eliminacji niepotrzebnych nawiasów. Na przykład następujące zapytanie LINQ:
await ctx.Customers
.Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName != null)
.ToListAsync();
Przekłada się na następującą usługę Azure SQL podczas korzystania z programu EF7:
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)
Co zostało ulepszone do następujących w przypadku korzystania z programu EF8:
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
Konkretna rezygnacja z klauzuli RETURNING/OUTPUT
Program EF7 zmienił domyślną aktualizację SQL, która ma być używana RETURNING
/OUTPUT
do pobierania kolumn wygenerowanych przez bazę danych. Niektóre przypadki, w których określono, gdzie to nie działa, a więc EF8 wprowadza jawne rezygnacje z tego zachowania.
Na przykład, aby zrezygnować z OUTPUT
korzystania z programu SQL Server/dostawcy usługi Azure SQL:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlOutputClause(false));
Lub zrezygnować z RETURNING
korzystania z dostawcy SQLite:
modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlReturningClause(false));
Inne drobne zmiany
Oprócz opisanych powyżej ulepszeń wprowadzono wiele mniejszych zmian wprowadzonych w programie EF8. Obejmuje to:
- Zgodność funkcji NativeAOT/trimming dla witryny Microsoft.Data.Sqlite
- Zezwalaj na regiony wieloregionowe lub preferowane przez aplikację w usłudze EF Core Cosmos
- SQLite: Dodaj ef. Functions.Unhex
- Opcje indeksu programu SQL Server SortInTempDB i DataCompression
- Zezwalaj na połączenie "unsharing" między kontekstami
- Dodawanie ogólnej wersji atrybutu EntityTypeConfiguration
- Zapytanie: dodaj obsługę projekcji jednostek JSON, które zostały skomponowane na
- Usuwanie niepotrzebnego podzapytania i projekcji podczas używania kolejności bez limitu/przesunięcia w operacjach zestawu
- Zezwalaj na buforowanie elementu DbContext za pomocą pojedynczych usług
- Opcjonalne restartSequenceOperation.StartValue
- Zezwalaj na właściwości UseSequence i HiLo w właściwościach innych niż klucz
- Podaj więcej informacji, gdy zostanie wygenerowany błąd "Nie znaleziono elementu DbContext"
- Przekazywanie zachowania śledzenia zapytań do przechwytywania materializacji
- Porównanie kluczy ciągu bez uwzględniania wielkości liter w programie SQL Server
- Zezwalaj konwerterom wartości na zmianę właściwości DbType
- Rozwiązywanie problemów z usługami aplikacji w usługach EF
- Zezwalaj na przenoszenie własności bazy danych DbConnection z aplikacji do obiektu DbContext