Szkolenie
Moduł
Modyfikowanie danych przy użyciu języka T-SQL - Training
Modyfikowanie danych przy użyciu języka T-SQL
Ta przeglądarka nie jest już obsługiwana.
Przejdź na przeglądarkę Microsoft Edge, aby korzystać z najnowszych funkcji, aktualizacji zabezpieczeń i pomocy technicznej.
Program EF Core 7.0 (EF7) został wydany w listopadzie 2022 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 EF7 jest przeznaczony dla platformy .NET 6 i może być używany z platformą .NET 6 (LTS) lub .NET 7.
Wiele z poniższych przykładów używa prostego modelu z blogami, wpisami, tagami i autorami:
public class Blog
{
public Blog(string name)
{
Name = name;
}
public int Id { get; private set; }
public string Name { get; set; }
public List<Post> Posts { get; } = new();
}
public class Post
{
public Post(string title, string content, DateTime publishedOn)
{
Title = title;
Content = content;
PublishedOn = publishedOn;
}
public int Id { get; private set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime PublishedOn { get; set; }
public Blog Blog { get; set; } = null!;
public List<Tag> Tags { get; } = new();
public Author? Author { get; set; }
public PostMetadata? Metadata { get; set; }
}
public class FeaturedPost : Post
{
public FeaturedPost(string title, string content, DateTime publishedOn, string promoText)
: base(title, content, publishedOn)
{
PromoText = promoText;
}
public string PromoText { get; set; }
}
public class Tag
{
public Tag(string id, string text)
{
Id = id;
Text = text;
}
public string Id { get; private set; }
public string Text { get; set; }
public List<Post> Posts { get; } = new();
}
public class Author
{
public Author(string name)
{
Name = name;
}
public int Id { get; private set; }
public string Name { get; set; }
public ContactDetails Contact { get; set; } = null!;
public List<Post> Posts { get; } = new();
}
Niektóre z przykładów używają również typów agregacji, które są mapowane na różne sposoby w różnych próbkach. Istnieje jeden typ agregacji dla kontaktów:
public class ContactDetails
{
public Address Address { get; set; } = null!;
public string? Phone { get; set; }
}
public class Address
{
public Address(string street, string city, string postcode, string country)
{
Street = street;
City = city;
Postcode = postcode;
Country = country;
}
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public string Country { get; set; }
}
Drugi typ agregacji dla metadanych post:
public class PostMetadata
{
public PostMetadata(int views)
{
Views = views;
}
public int Views { get; set; }
public List<SearchTerm> TopSearches { get; } = new();
public List<Visits> TopGeographies { get; } = new();
public List<PostUpdate> Updates { get; } = new();
}
public class SearchTerm
{
public SearchTerm(string term, int count)
{
Term = term;
Count = count;
}
public string Term { get; private set; }
public int Count { get; private set; }
}
public class Visits
{
public Visits(double latitude, double longitude, int count)
{
Latitude = latitude;
Longitude = longitude;
Count = count;
}
public double Latitude { get; private set; }
public double Longitude { get; private set; }
public int Count { get; private set; }
public List<string>? Browsers { get; set; }
}
public class PostUpdate
{
public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
{
PostedFrom = postedFrom;
UpdatedOn = updatedOn;
}
public IPAddress PostedFrom { get; private set; }
public string? UpdatedBy { get; init; }
public DateTime UpdatedOn { get; private set; }
public List<Commit> Commits { get; } = new();
}
public class Commit
{
public Commit(DateTime committedOn, string comment)
{
CommittedOn = committedOn;
Comment = comment;
}
public DateTime CommittedOn { get; private set; }
public string Comment { get; set; }
}
Napiwek
Przykładowy model można znaleźć w BlogsContext.cs.
Większość relacyjnych baz danych obsługuje kolumny zawierające dokumenty JSON. Dane JSON w tych kolumnach można przechodzić do szczegółów za pomocą zapytań. Umożliwia to na przykład filtrowanie i sortowanie według elementów dokumentów, a także projekcję elementów z dokumentów w wynikach. Kolumny JSON umożliwiają relacyjnym bazom danych przejmowanie niektórych cech baz danych dokumentów, tworząc przydatną hybrydę między nimi.
Program EF7 zawiera niezależną od dostawcy obsługę kolumn JSON z implementacją programu SQL Server. Ta obsługa umożliwia mapowanie agregacji utworzonych na podstawie typów platformy .NET do dokumentów JSON. Normalne zapytania LINQ mogą być używane w agregacjach i zostaną one przetłumaczone na odpowiednie konstrukcje zapytań potrzebne do przechodzenia do szczegółów w formacie JSON. Program EF7 obsługuje również aktualizowanie i zapisywanie zmian w dokumentach JSON.
Uwaga
Obsługa formatu JSON w formacie SQLite jest planowana na post EF7. Dostawcy PostgreSQL i Pomelo MySQL już obsługują kolumny JSON. Będziemy współpracować z autorami tych dostawców w celu dostosowania obsługi kodu JSON we wszystkich dostawcach.
W programie EF Core typy agregacji są definiowane przy użyciu elementów OwnsOne
i OwnsMany
. Rozważmy na przykład typ agregacji z naszego przykładowego modelu używanego do przechowywania informacji kontaktowych:
public class ContactDetails
{
public Address Address { get; set; } = null!;
public string? Phone { get; set; }
}
public class Address
{
public Address(string street, string city, string postcode, string country)
{
Street = street;
City = city;
Postcode = postcode;
Country = country;
}
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public string Country { get; set; }
}
Można go następnie użyć w typie jednostki "właściciel", na przykład do przechowywania danych kontaktowych autora:
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ContactDetails Contact { get; set; }
}
Typ agregacji jest skonfigurowany przy OnModelCreating
użyciu polecenia OwnsOne
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Napiwek
Pokazany tutaj kod pochodzi z JsonColumnsSample.cs.
Domyślnie dostawcy relacyjnej bazy danych mapują typy agregacji, takie jak ta, do tej samej tabeli co typ jednostki będąca właścicielem. Oznacza to, że każda właściwość ContactDetails
klas i Address
jest mapowana na kolumnę w Authors
tabeli.
Niektórzy zapisani autorzy z danymi kontaktowymi będą wyglądać następująco:
Autorzy
Id | Nazwisko | Contact_Address_Street | Contact_Address_City | Contact_Address_Postcode | Contact_Address_Country | Contact_Phone |
---|---|---|---|---|---|---|
1 | Maddy Montaquila | 1 Główny St | Camberwick Zielony | CW1 5ZH | Zjednoczone Królestwo | 01632 12345 |
2 | Jeremy Likness | 2 St | Chigley | CW1 5ZH | Zjednoczone Królestwo | 01632 12346 |
3 | Daniel Roth | 3 St | Camberwick Zielony | CW1 5ZH | Zjednoczone Królestwo | 01632 12347 |
100 | Arthur Vickers | 15a Main St | Chigley | CW1 5ZH | Zjednoczone Królestwo | 01632 22345 |
5 | Brice Lambson | 4 Main St | Chigley | CW1 5ZH | Zjednoczone Królestwo | 01632 12349 |
W razie potrzeby każdy typ jednostki tworzący agregację można zamapować na własną tabelę:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToTable("Contacts");
ownedNavigationBuilder.OwnsOne(
contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder =>
{
ownedOwnedNavigationBuilder.ToTable("Addresses");
});
});
}
Te same dane są następnie przechowywane w trzech tabelach:
Autorzy
Id | Nazwisko |
---|---|
1 | Maddy Montaquila |
2 | Jeremy Likness |
3 | Daniel Roth |
100 | Arthur Vickers |
5 | Brice Lambson |
Kontakty
Identyfikator autora | Telefonu |
---|---|
1 | 01632 12345 |
2 | 01632 12346 |
3 | 01632 12347 |
100 | 01632 22345 |
5 | 01632 12349 |
Adresy
ContactDetailsAuthorId | Ulica | City | Kodów pocztowych | Kraj |
---|---|---|---|---|
1 | 1 Główny St | Camberwick Zielony | CW1 5ZH | Zjednoczone Królestwo |
2 | 2 St | Chigley | CW1 5ZH | Zjednoczone Królestwo |
3 | 3 St | Camberwick Zielony | CW1 5ZH | Zjednoczone Królestwo |
100 | 15a Main St | Chigley | CW1 5ZH | Zjednoczone Królestwo |
5 | 4 Main St | Chigley | CW1 5ZH | Zjednoczone Królestwo |
Teraz, w interesującej części. W programie EF7 ContactDetails
typ agregacji można zamapować na kolumnę JSON. Wymaga to tylko jednego wywołania ToJson()
podczas konfigurowania typu agregacji:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToJson();
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Tabela Authors
będzie teraz zawierać kolumnę JSON wypełniona ContactDetails
dokumentem JSON dla każdego autora:
Autorzy
Id | Nazwisko | Kontakt biznesowy |
---|---|---|
1 | Maddy Montaquila | { "Phone":"01632 12345", "Adres": { "City":"Camberwick Green", "Country":"UK", "Kod pocztowy":"CW1 5ZH", "Street":"1 Main St" } } |
2 | Jeremy Likness | { "Phone":"01632 12346", "Adres": { "Miasto":"Chigley", "Country":"UK", "Kod pocztowy":"CH1 5ZH", "Street":"2 Main St" } } |
3 | Daniel Roth | { "Phone":"01632 12347", "Adres": { "City":"Camberwick Green", "Country":"UK", "Kod pocztowy":"CW1 5ZH", "Street":"3 Main St" } } |
100 | Arthur Vickers | { "Phone":"01632 12348", "Adres": { "Miasto":"Chigley", "Country":"UK", "Kod pocztowy":"CH1 5ZH", "Street":"15a Main St" } } |
5 | Brice Lambson | { "Phone":"01632 12349", "Adres": { "Miasto":"Chigley", "Country":"UK", "Kod pocztowy":"CH1 5ZH", "Street":"4 Main St" } } |
Napiwek
To użycie agregacji jest bardzo podobne do sposobu mapowania dokumentów JSON podczas korzystania z dostawcy EF Core dla usługi Azure Cosmos DB. Kolumny JSON umożliwiają używanie programu EF Core względem baz danych dokumentów do dokumentów osadzonych w relacyjnej bazie danych.
Powyższe dokumenty JSON są bardzo proste, ale ta funkcja mapowania może być również używana z bardziej złożonymi strukturami dokumentów. Rozważmy na przykład inny typ agregacji z naszego przykładowego modelu, który służy do reprezentowania metadanych dotyczących wpisu:
public class PostMetadata
{
public PostMetadata(int views)
{
Views = views;
}
public int Views { get; set; }
public List<SearchTerm> TopSearches { get; } = new();
public List<Visits> TopGeographies { get; } = new();
public List<PostUpdate> Updates { get; } = new();
}
public class SearchTerm
{
public SearchTerm(string term, int count)
{
Term = term;
Count = count;
}
public string Term { get; private set; }
public int Count { get; private set; }
}
public class Visits
{
public Visits(double latitude, double longitude, int count)
{
Latitude = latitude;
Longitude = longitude;
Count = count;
}
public double Latitude { get; private set; }
public double Longitude { get; private set; }
public int Count { get; private set; }
public List<string>? Browsers { get; set; }
}
public class PostUpdate
{
public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
{
PostedFrom = postedFrom;
UpdatedOn = updatedOn;
}
public IPAddress PostedFrom { get; private set; }
public string? UpdatedBy { get; init; }
public DateTime UpdatedOn { get; private set; }
public List<Commit> Commits { get; } = new();
}
public class Commit
{
public Commit(DateTime committedOn, string comment)
{
CommittedOn = committedOn;
Comment = comment;
}
public DateTime CommittedOn { get; private set; }
public string Comment { get; set; }
}
Ten typ agregacji zawiera kilka zagnieżdżonych typów i kolekcji. Wywołania i OwnsOne
OwnsMany
są używane do mapowania tego typu agregacji:
modelBuilder.Entity<Post>().OwnsOne(
post => post.Metadata, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToJson();
ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches);
ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies);
ownedNavigationBuilder.OwnsMany(
metadata => metadata.Updates,
ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits));
});
Napiwek
ToJson
element jest wymagany tylko w zagregowanym katalogu głównym, aby zamapować całą agregację na dokument JSON.
Dzięki temu mapowaniu program EF7 może tworzyć zapytania i wykonywać zapytania w złożonym dokumencie JSON w następujący sposób:
{
"Views": 5085,
"TopGeographies": [
{
"Browsers": "Firefox, Netscape",
"Count": 924,
"Latitude": 110.793,
"Longitude": 39.2431
},
{
"Browsers": "Firefox, Netscape",
"Count": 885,
"Latitude": 133.793,
"Longitude": 45.2431
}
],
"TopSearches": [
{
"Count": 9359,
"Term": "Search #1"
}
],
"Updates": [
{
"PostedFrom": "127.0.0.1",
"UpdatedBy": "Admin",
"UpdatedOn": "1996-02-17T19:24:29.5429092Z",
"Commits": []
},
{
"PostedFrom": "127.0.0.1",
"UpdatedBy": "Admin",
"UpdatedOn": "2019-11-24T19:24:29.5429093Z",
"Commits": [
{
"Comment": "Commit #1",
"CommittedOn": "2022-08-21T00:00:00+01:00"
}
]
},
{
"PostedFrom": "127.0.0.1",
"UpdatedBy": "Admin",
"UpdatedOn": "1997-05-28T19:24:29.5429097Z",
"Commits": [
{
"Comment": "Commit #1",
"CommittedOn": "2022-08-21T00:00:00+01:00"
},
{
"Comment": "Commit #2",
"CommittedOn": "2022-08-21T00:00:00+01:00"
}
]
}
]
}
Uwaga
Mapowanie typów przestrzennych bezpośrednio do formatu JSON nie jest jeszcze obsługiwane. Powyższy dokument używa double
wartości jako obejścia. Zagłosuj na typy przestrzenne pomocy technicznej w kolumnach JSON, jeśli jest to coś, co cię interesuje.
Uwaga
Kolekcje mapowania typów pierwotnych na format JSON nie są jeszcze obsługiwane. W powyższym dokumencie użyto konwertera wartości, aby przekształcić kolekcję w ciąg rozdzielony przecinkami. Zagłosuj na Json: dodaj obsługę kolekcji typów pierwotnych, jeśli jest to coś, co cię interesuje.
Uwaga
Mapowanie typów należących do formatu JSON nie jest jeszcze obsługiwane w połączeniu z dziedziczeniem TPT lub TPC. Zagłosuj na właściwości JSON obsługi za pomocą mapowania dziedziczenia TPT/TPC, jeśli jest to coś, co cię interesuje.
Zapytania w kolumnach JSON działają tak samo jak wykonywanie zapytań w dowolnym innym typie agregacji w programie EF Core. Oznacza to, że wystarczy użyć LINQ! Oto kilka przykładów.
Zapytanie dla wszystkich autorów, którzy mieszkają w Chigley:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
To zapytanie generuje następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'
Zwróć uwagę na użycie polecenia JSON_VALUE
, aby pobrać element City
z Address
wewnątrz dokumentu JSON.
Select
Umożliwia wyodrębnianie elementów projektu i wyodrębnianie ich z dokumentu JSON:
var postcodesInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.Select(author => author.Contact.Address.Postcode)
.ToListAsync();
To zapytanie generuje następujący kod SQL:
SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'
Oto przykład, który wykonuje nieco więcej w filtrze i projekcji, a także zamówienia według numeru telefonu w dokumencie JSON:
var orderedAddresses = await context.Authors
.Where(
author => (author.Contact.Address.City == "Chigley"
&& author.Contact.Phone != null)
|| author.Name.StartsWith("D"))
.OrderBy(author => author.Contact.Phone)
.Select(
author => author.Name + " (" + author.Contact.Address.Street
+ ", " + author.Contact.Address.City
+ " " + author.Contact.Address.Postcode + ")")
.ToListAsync();
To zapytanie generuje następujący kod SQL:
SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')'
FROM [Authors] AS [a]
WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%')
ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max))
A gdy dokument JSON zawiera kolekcje, można je rzutować w wynikach:
var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
.AsNoTracking()
.Select(
post => new
{
post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
})
.ToListAsync();
To zapytanie generuje następujący kod SQL:
SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000
Uwaga
Bardziej złożone zapytania obejmujące kolekcje JSON wymagają jsonpath
obsługi. Zagłosuj na zapytanie dotyczące ścieżki jsonpath pomocy technicznej, jeśli jest to coś, co cię interesuje.
Napiwek
Rozważ utworzenie indeksów w celu zwiększenia wydajności zapytań w dokumentach JSON. Na przykład zobacz Indeksowanie danych Json podczas korzystania z programu SQL Server.
SaveChanges
i SaveChangesAsync
działają w normalny sposób, aby wprowadzać aktualizacje do kolumny JSON. W przypadku rozbudowanych zmian cały dokument zostanie zaktualizowany. Na przykład zastąpienie większości Contact
dokumentu dla autora:
var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));
jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };
await context.SaveChangesAsync();
W takim przypadku cały nowy dokument jest przekazywany jako parametr:
info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30']
Który jest następnie używany w języku UPDATE
SQL:
UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Jeśli jednak tylko dokument podrzędny zostanie zmieniony, program EF Core użyje JSON_MODIFY
polecenia w celu zaktualizowania tylko dokumentu podrzędnego. Na przykład zmiana Address
wewnątrz Contact
dokumentu:
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
await context.SaveChangesAsync();
Generuje następujące parametry:
info: 10/2/2022 15:51:15.895 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30']
Który jest używany w UPDATE
wywołaniu JSON_MODIFY
:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;
Na koniec, jeśli tylko jedna właściwość zostanie zmieniona, program EF Core ponownie użyje polecenia "JSON_MODIFY", tym razem, aby zastosować poprawkę tylko zmienionej wartości właściwości. Na przykład:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
Generuje następujące parametry:
info: 10/2/2022 15:54:05.112 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']
Które są ponownie używane z elementem JSON_MODIFY
:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
Domyślnie program EF Core śledzi zmiany w jednostkach, a następnie wysyła aktualizacje do bazy danych po wywołaniu jednej z SaveChanges
metod. Zmiany są wysyłane tylko dla właściwości i relacji, które rzeczywiście uległy zmianie. Ponadto śledzone jednostki pozostają zsynchronizowane ze zmianami wysłanymi do bazy danych. Ten mechanizm jest wydajnym i wygodnym sposobem wysyłania wstawień, aktualizacji i usuwania ogólnego przeznaczenia do bazy danych. Te zmiany są również dzielone na partie w celu zmniejszenia liczby rund bazy danych.
Jednak czasami przydatne jest wykonywanie poleceń aktualizacji lub usuwania w bazie danych bez angażowania monitora zmian. Program EF7 umożliwia korzystanie z nowych ExecuteUpdate metod i ExecuteDelete . Te metody są stosowane do zapytania LINQ i będą aktualizować lub usuwać jednostki w bazie danych na podstawie wyników tego zapytania. Wiele jednostek można zaktualizować za pomocą jednego polecenia, a jednostki nie są ładowane do pamięci, co oznacza, że może to spowodować bardziej wydajne aktualizacje i usunięcie.
Należy jednak pamiętać, że:
Wszystko to oznacza, że ExecuteUpdate
metody i ExecuteDelete
uzupełniają, a nie zastępują istniejącego SaveChanges
mechanizmu.
Napiwek
Pokazany tutaj kod pochodzi z ExecuteDeleteSample.cs.
Wywołanie ExecuteDelete
metody lub ExecuteDeleteAsync
na DbSet
serwerze natychmiast usuwa wszystkie jednostki z DbSet
bazy danych. Aby na przykład usunąć wszystkie Tag
jednostki:
await context.Tags.ExecuteDeleteAsync();
Spowoduje to wykonanie następującego kodu SQL podczas korzystania z programu SQL Server:
DELETE FROM [t]
FROM [Tags] AS [t]
Co ciekawe, zapytanie może zawierać filtr. Na przykład:
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();
Spowoduje to wykonanie następującego kodu SQL:
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'
Zapytanie może również używać bardziej złożonych filtrów, w tym nawigacji do innych typów. Aby na przykład usunąć tagi tylko ze starych wpisów w blogu:
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();
Które wykonuje:
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))
Napiwek
Pokazany tutaj kod pochodzi z ExecuteUpdateSample.cs.
ExecuteUpdate
i ExecuteUpdateAsync
zachowują się w bardzo podobny sposób do ExecuteDelete
metod. Główną różnicą jest to, że aktualizacja wymaga znajomości właściwości do zaktualizowania i sposobu ich aktualizowania. Jest to osiągane przy użyciu co najmniej jednego wywołania metody SetProperty
. Na przykład, aby zaktualizować każdy Name
blog:
await context.Blogs.ExecuteUpdateAsync(
s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));
Pierwszy parametr określa SetProperty
, która właściwość ma być aktualizowana; w tym przypadku Blog.Name
. Drugi parametr określa sposób obliczania nowej wartości; w tym przypadku przez pobranie istniejącej wartości i dołączenie "*Featured!*"
elementu . Wynikowy kod SQL to:
UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]
Podobnie jak w przypadku ExecuteDelete
elementu , zapytanie może służyć do filtrowania, które jednostki są aktualizowane. Ponadto wiele wywołań programu SetProperty
może służyć do aktualizowania więcej niż jednej właściwości w jednostce docelowej. Na przykład, aby zaktualizować Title
i Content
wszystkich wpisów opublikowanych przed 2022 r.:
await context.Posts
.Where(p => p.PublishedOn.Year < 2022)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
.SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));
W takim przypadku wygenerowany język SQL jest nieco bardziej skomplikowany:
UPDATE [p]
SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
[p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022
Na koniec, podobnie jak w przypadku ExecuteDelete
metody , filtr może odwoływać się do innych tabel. Aby na przykład zaktualizować wszystkie tagi ze starych wpisów:
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
Które generuje:
UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))
Aby uzyskać więcej informacji i przykładów kodu w systemach ExecuteUpdate
i ExecuteDelete
, zobacz ExecuteUpdate i ExecuteDelete.
ExecuteUpdate
i ExecuteDelete
może działać tylko na jednej tabeli. Ma to wpływ na pracę z różnymi strategiami mapowania dziedziczenia. Ogólnie rzecz biorąc, podczas korzystania ze strategii mapowania TPH nie ma problemów, ponieważ istnieje tylko jedna tabela do zmodyfikowania. Na przykład usunięcie wszystkich FeaturedPost
jednostek:
await context.Set<FeaturedPost>().ExecuteDeleteAsync();
Generuje następujący kod SQL podczas korzystania z mapowania TPH:
DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'
W przypadku korzystania ze strategii mapowania TPC nie ma również żadnych problemów, ponieważ potrzebne są tylko zmiany w pojedynczej tabeli:
DELETE FROM [f]
FROM [FeaturedPosts] AS [f]
Jednak próba użycia strategii mapowania TPT zakończy się niepowodzeniem, ponieważ wymaga usunięcia wierszy z dwóch różnych tabel.
Dodanie filtru do zapytania często oznacza, że operacja zakończy się niepowodzeniem zarówno ze strategiami TPC, jak i TPT. Jest to ponownie spowodowane koniecznością usunięcia wierszy z wielu tabel. Poniższe przykładowe zapytanie:
await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();
Generuje następujący kod SQL podczas korzystania z funkcji TPH:
DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')
Ale kończy się niepowodzeniem w przypadku korzystania z TPC lub TPT.
Napiwek
Problem nr 10879 śledzi dodawanie obsługi automatycznego wysyłania wielu poleceń w tych scenariuszach. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.
Jak wspomniano powyżej, może być konieczne usunięcie lub zaktualizowanie jednostek zależnych przed usunięciem podmiotu zabezpieczeń relacji. Na przykład każda z nich Post
jest zależna od skojarzonej z nią wartości Author
. Oznacza to, że nie można usunąć autora, jeśli wpis nadal odwołuje się do niego; Spowoduje to naruszenie ograniczenia klucza obcego w bazie danych. Na przykład próba:
await context.Authors.ExecuteDeleteAsync();
Spowoduje to następujący wyjątek w programie SQL Server:
Microsoft.Data.SqlClient.SqlException (0x80131904): instrukcja DELETE powoduje konflikt z ograniczeniem REFERENCE "FK_Posts_Authors_AuthorId". Konflikt wystąpił w bazie danych "TphBlogsContext", tabeli "dbo". Posty", kolumna "AuthorId". Instrukcja została zakończona.
Aby rozwiązać ten problem, musimy najpierw usunąć wpisy lub zerwać relację między każdym wpisem a jego autorem, ustawiając AuthorId
właściwość klucza obcego na null. Na przykład przy użyciu opcji usuwania:
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
Napiwek
TagWith
Może służyć do tagowania ExecuteDelete
lub ExecuteUpdate
w taki sam sposób, jak taguje normalne zapytania.
Spowoduje to wykonanie dwóch oddzielnych poleceń; pierwszy, aby usunąć zależności:
-- Deleting posts...
DELETE FROM [p]
FROM [Posts] AS [p]
A drugi, aby usunąć podmioty zabezpieczeń:
-- Deleting authors...
DELETE FROM [a]
FROM [Authors] AS [a]
Ważne
Wiele ExecuteDelete
poleceń i ExecuteUpdate
nie będzie domyślnie zawartych w jednej transakcji. Interfejsy API transakcji DbContext mogą być jednak używane w normalny sposób, aby opakowować te polecenia w transakcji.
Napiwek
Wysyłanie tych poleceń w jednej rundzie zależy od problemu #10879. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.
Konfigurowanie usuwania kaskadowego w bazie danych może być bardzo przydatne tutaj. W naszym modelu wymagana jest relacja między elementami Blog
i Post
, co powoduje skonfigurowanie kaskadowego usuwania przez platformę EF Core zgodnie z konwencją. Oznacza to, że gdy blog zostanie usunięty z bazy danych, wszystkie jego zależne wpisy również zostaną usunięte. Następnie następuje usunięcie wszystkich blogów i wpisów, których potrzebujemy tylko do usunięcia blogów:
await context.Blogs.ExecuteDeleteAsync();
Spowoduje to wykonanie następujących czynności w języku SQL:
DELETE FROM [b]
FROM [Blogs] AS [b]
Co, ponieważ usuwa blog, spowoduje również usunięcie wszystkich powiązanych wpisów przez skonfigurowane kaskadowe usunięcie.
W ef7 wydajność SaveChanges i SaveChangesAsync została znacznie zwiększona. W niektórych scenariuszach zapisywanie zmian jest teraz maksymalnie cztery razy szybsze niż w przypadku programu EF Core 6.0!
Większość tych ulepszeń pochodzi z następujących elementów:
Poniżej przedstawiono kilka przykładów tych ulepszeń.
Uwaga
Zobacz Ogłoszenie programu Entity Framework Core 7 Preview 6: Performance Edition na blogu platformy .NET, aby zapoznać się ze szczegółowym omówieniem tych zmian.
Napiwek
Pokazany tutaj kod pochodzi z SaveChangesPerformanceSample.cs.
Wszystkie nowoczesne relacyjne bazy danych gwarantują transakcyjność (większość) pojedynczych instrukcji SQL. Oznacza to, że instrukcja nigdy nie zostanie ukończona tylko częściowo, nawet jeśli wystąpi błąd. Program EF7 unika uruchamiania jawnej transakcji w tych przypadkach.
Na przykład zapoznaj się z rejestrowaniem dla następującego wywołania metody SaveChanges
:
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
Pokazuje, że w programie EF Core 6.0 polecenie jest opakowane przez polecenia, INSERT
aby rozpocząć, a następnie zatwierdzić transakcję:
dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
Program EF7 wykrywa, że transakcja nie jest tutaj potrzebna, a więc usuwa następujące wywołania:
info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@p0);
Spowoduje to usunięcie dwóch rund baz danych, co może mieć ogromną różnicę w ogólnej wydajności, zwłaszcza gdy opóźnienie wywołań bazy danych jest wysokie. W typowych systemach produkcyjnych baza danych nie znajduje się na tej samej maszynie co aplikacja. Oznacza to, że opóźnienie jest często stosunkowo wysokie, co czyni tę optymalizację szczególnie skuteczną w rzeczywistych systemach produkcyjnych.
Powyższy przypadek wstawia pojedynczy wiersz z kolumną IDENTITY
klucza i nie ma innych wartości wygenerowanych przez bazę danych. Program EF7 upraszcza język SQL w tym przypadku przy użyciu polecenia OUTPUT INSERTED
. Chociaż to uproszczenie nie jest prawidłowe w wielu innych przypadkach, nadal ważne jest, aby poprawić, ponieważ tego rodzaju wstawianie jednorzędowe jest bardzo powszechne w wielu aplikacjach.
W programie EF Core 6.0 domyślne podejście do wstawiania wielu wierszy było spowodowane ograniczeniami obsługi programu SQL Server dla tabel z wyzwalaczami. Chcieliśmy upewnić się, że środowisko domyślne działało nawet dla mniejszości użytkowników z wyzwalaczami w tabelach. Oznaczało to, że nie można użyć prostej OUTPUT
klauzuli, ponieważ w programie SQL Server nie działa to z wyzwalaczami. Zamiast tego podczas wstawiania wielu jednostek program EF Core 6.0 wygenerował nieco dość splotowy kod SQL. Na przykład to wywołanie metody :SaveChanges
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
Wyniki następujących akcji podczas uruchamiania względem programu SQL Server z programem EF Core 6.0:
dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;
SELECT [i].[Id] FROM @inserted0 i
ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
Ważne
Mimo że jest to skomplikowane, przetwarzanie wsadowe wielu wstawek w ten sposób jest nadal znacznie szybsze niż wysyłanie pojedynczego polecenia dla każdego wstawiania.
W programie EF7 nadal można uzyskać ten język SQL, jeśli tabele zawierają wyzwalacze, ale w przypadku typowego przypadku generujemy znacznie wydajniejsze polecenia, jeśli nadal są one nieco złożone:
info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;
Transakcja nie istnieje, podobnie jak w przypadku pojedynczego wstawiania, ponieważ MERGE
jest pojedynczą instrukcją chronioną przez niejawną transakcję. Ponadto tabela tymczasowa nie istnieje, a klauzula OUTPUT wysyła teraz wygenerowane identyfikatory bezpośrednio do klienta. Może to być cztery razy szybsze niż w programie EF Core 6.0, w zależności od czynników środowiskowych, takich jak opóźnienie między aplikacją a bazą danych.
Jeśli tabela zawiera wyzwalacze, wywołanie metody SaveChanges
w powyższym kodzie zgłosi wyjątek:
Nieobsługiwany wyjątek. Microsoft.EntityFrameworkCore.DbUpdateException:
Nie można zapisać zmian, ponieważ tabela docelowa zawiera wyzwalacze bazy danych. Aby uzyskać więcej informacji, skonfigurujhttps://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers
odpowiedni typ jednostki.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
tabela docelowa "BlogsWithTriggers" instrukcji DML nie może mieć żadnych włączonych wyzwalaczy, jeśli instrukcja zawiera klauzulę OUTPUT bez klauzuli INTO.
Poniższy kod może służyć do informowania platformy EF Core o tym, że tabela ma wyzwalacz:
modelBuilder
.Entity<BlogWithTrigger>()
.ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));
Program EF7 powróci do programu EF Core 6.0 SQL podczas wysyłania poleceń wstawiania i aktualizowania tej tabeli.
Aby uzyskać więcej informacji, w tym konwencję automatycznego konfigurowania wszystkich mapowanych tabel z wyzwalaczami, zobacz Tabele programu SQL Server z wyzwalaczami wymagają teraz specjalnej konfiguracji programu EF Core w dokumentacji zmian powodujących niezgodność w programie EF7.
Rozważ wstawienie grafu jednostek zawierających nową jednostkę główną, a także nowe jednostki zależne z kluczami obcymi odwołujące się do nowego podmiotu zabezpieczeń. Na przykład:
await context.AddAsync(
new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();
Jeśli klucz podstawowy podmiotu zabezpieczeń jest generowany przez bazę danych, wartość ustawiona dla klucza obcego w zależnych nie jest znana, dopóki podmiot zabezpieczeń nie zostanie wstawiony. Program EF Core generuje dwa rundy dla tego - jeden w celu wstawienia podmiotu zabezpieczeń i odzyskania nowego klucza podstawowego, a drugi w celu wstawienia zależności z zestawem wartości klucza obcego. A ponieważ istnieją dwie instrukcje dotyczące tego, wymagana jest transakcja, co oznacza, że istnieją w sumie cztery rundy:
dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@p0);
info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [Post] USING (
VALUES (@p1, @p2, 0),
(@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([BlogId], [Title])
VALUES (i.[BlogId], i.[Title])
OUTPUT INSERTED.[Id], i._Position;
dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
Jednak w niektórych przypadkach wartość klucza podstawowego jest znana przed wstawieniem podmiotu zabezpieczeń. Obejmuje to:
W programie EF7 te przypadki są teraz zoptymalizowane pod kątem jednej rundy. Na przykład w powyższym przypadku w programie SQL Server Blog.Id
klucz podstawowy można skonfigurować do używania strategii generowania hi-lo:
modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();
Wywołanie SaveChanges
z góry jest teraz zoptymalizowane pod kątem pojedynczej rundy dla wstawiania.
dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Id], [Name])
VALUES (@p0, @p1);
INSERT INTO [Posts] ([Id], [BlogId], [Title])
VALUES (@p2, @p3, @p4),
(@p5, @p6, @p7);
dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
Zwróć uwagę, że transakcja jest nadal potrzebna tutaj. Dzieje się tak, ponieważ wstawki są tworzone w dwóch oddzielnych tabelach.
Program EF7 używa również jednej partii w innych przypadkach, w których program EF Core 6.0 tworzy więcej niż jedną partię. Na przykład podczas usuwania i wstawiania wierszy do tej samej tabeli.
Jak pokazano w niektórych przykładach, zapisywanie wyników w bazie danych może być złożoną działalnością biznesową. W tym miejscu użycie czegoś takiego jak EF Core naprawdę pokazuje jego wartość. EF Core:
Ponadto różne systemy baz danych wymagają różnych baz danych SQL dla wielu z tych przypadków. Dostawca bazy danych EF Core współpracuje z platformą EF Core, aby zapewnić wysyłanie poprawnych i wydajnych poleceń dla każdego przypadku.
Domyślnie program EF Core mapuje hierarchię dziedziczenia typów platformy .NET na jedną tabelę bazy danych. Jest to nazywane strategią mapowania tabeli na hierarchię (TPH ). Program EF Core 5.0 wprowadził strategię typu tabeli (TPT ), która obsługuje mapowanie każdego typu platformy .NET na inną tabelę bazy danych. Program EF7 wprowadza strategię TPC (table-per-concrete-type). TPC mapuje również typy platformy .NET na różne tabele, ale w sposób, który rozwiązuje niektóre typowe problemy z wydajnością strategii TPT.
Napiwek
Pokazany tutaj kod pochodzi z TpcInheritanceSample.cs.
Napiwek
Zespół EF pokazał i szczegółowo omówił mapowanie TPC w odcinku standupu społeczności danych platformy .NET. Podobnie jak w przypadku wszystkich odcinków Community Standup, możesz obejrzeć odcinek TPC teraz na YouTube.
Strategia TPC jest podobna do strategii TPT, z tą różnicą, że dla każdego konkretnego typu w hierarchii tworzona jest inna tabela, ale tabele nie są tworzone dla typów abstrakcyjnych — stąd nazwa "tabela-typ-". Podobnie jak w przypadku TPT, sama tabela wskazuje typ zapisanego obiektu. Jednak w przeciwieństwie do mapowania TPT każda tabela zawiera kolumny dla każdej właściwości w typie i jego typach podstawowych. Schematy bazy danych TPC są zdenormalizowane.
Rozważ na przykład mapowanie tej hierarchii:
public abstract class Animal
{
protected Animal(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }
public Food? Food { get; set; }
}
public abstract class Pet : Animal
{
protected Pet(string name)
: base(name)
{
}
public string? Vet { get; set; }
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class FarmAnimal : Animal
{
public FarmAnimal(string name, string species)
: base(name)
{
Species = species;
}
public override string Species { get; }
[Precision(18, 2)]
public decimal Value { get; set; }
public override string ToString()
=> $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Cat : Pet
{
public Cat(string name, string educationLevel)
: base(name)
{
EducationLevel = educationLevel;
}
public string EducationLevel { get; set; }
public override string Species => "Felis catus";
public override string ToString()
=> $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Dog : Pet
{
public Dog(string name, string favoriteToy)
: base(name)
{
FavoriteToy = favoriteToy;
}
public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";
public override string ToString()
=> $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Human : Animal
{
public Human(string name)
: base(name)
{
}
public override string Species => "Homo sapiens";
public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();
public override string ToString()
=> $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
$" eats {Food?.ToString() ?? "<Unknown>"}";
}
W przypadku korzystania z programu SQL Server tabele utworzone dla tej hierarchii to:
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));
CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));
CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));
CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));
Zwróć uwagę, że:
Nie ma tabel dla Animal
typów lub Pet
, ponieważ znajdują się abstract
one w modelu obiektów. Należy pamiętać, że język C# nie zezwala na wystąpienia typów abstrakcyjnych i dlatego nie ma sytuacji, w której wystąpienie typu abstrakcyjnego zostanie zapisane w bazie danych.
Mapowanie właściwości w typach podstawowych jest powtarzane dla każdego konkretnego typu. Na przykład każda tabela ma kolumnę Name
, a zarówno Koty, jak i Psy mają kolumnę Vet
.
Zapisanie niektórych danych w tej bazie danych powoduje wykonanie następujących czynności:
Tabela Cats
Id | Nazwisko | Identyfikator żywności | Weterynarz | EducationLevel |
---|---|---|---|---|
1 | Alicja | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA |
2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Przedszkole |
8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Stół dla psów
Id | Nazwisko | Identyfikator żywności | Weterynarz | UlubioneToy |
---|---|---|---|---|
3 | Grzanka | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Pan Wiewiórka |
Tabela FarmAnimals
Id | Nazwisko | Identyfikator żywności | Wartość | Gatunki |
---|---|---|---|---|
100 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus |
Stół dla ludzi
Id | Nazwisko | Identyfikator żywności | FavoriteAnimalId |
---|---|---|---|
5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
9 | Katie | null | 8 |
Zwróć uwagę, że w przeciwieństwie do mapowania TPT wszystkie informacje dotyczące pojedynczego obiektu znajdują się w jednej tabeli. I, w przeciwieństwie do mapowania TPH, nie ma kombinacji kolumn i wierszy w żadnej tabeli, w której nigdy nie jest używany przez model. Poniżej zobaczymy, jak te cechy mogą być ważne dla zapytań i magazynu.
Wszystkie typy w hierarchii dziedziczenia muszą być jawnie uwzględnione w modelu podczas mapowania hierarchii za pomocą programu EF Core. Można to zrobić, tworząc DbSet
właściwości dla DbContext
każdego typu:
public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Pet> Pets => Set<Pet>();
public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
public DbSet<Cat> Cats => Set<Cat>();
public DbSet<Dog> Dogs => Set<Dog>();
public DbSet<Human> Humans => Set<Human>();
Lub przy użyciu metody w pliku Entity
OnModelCreating
:
modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();
Ważne
Różni się to od starszego zachowania EF6, w którym typy pochodne mapowanych typów bazowych zostaną automatycznie odnalezione, jeśli znajdują się w tym samym zestawie.
Nie trzeba nic innego robić, aby mapować hierarchię jako TPH, ponieważ jest to strategia domyślna. Jednak począwszy od ef7, funkcja TPH może być jawna przez wywołanie UseTphMappingStrategy
podstawowego typu hierarchii:
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
Aby zamiast tego użyć TPT, zmień wartość na UseTptMappingStrategy
:
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
Analogicznie, UseTpcMappingStrategy
służy do konfigurowania TPC:
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
W każdym przypadku nazwa tabeli używana dla każdego typu jest pobierana z DbSet
nazwy właściwości w DbContext
obiekcie lub można skonfigurować przy użyciu ToTable
metody konstruktora lub atrybutu [Table]
.
W przypadku zapytań strategia TPC jest ulepszeniem TPT, ponieważ gwarantuje, że informacje dla danego wystąpienia jednostki są zawsze przechowywane w jednej tabeli. Oznacza to, że strategia TPC może być przydatna, gdy zamapowana hierarchia jest duża i ma wiele typów (zwykle liści), z których każda ma dużą liczbę właściwości i gdzie w większości zapytań jest używany tylko niewielki podzbiór typów.
Język SQL wygenerowany dla trzech prostych zapytań LINQ może służyć do obserwowania, gdzie TPC działa dobrze w porównaniu z TPH i TPT. Te zapytania to:
Zapytanie zwracające jednostki wszystkich typów w hierarchii:
context.Animals.ToList();
Zapytanie zwracające jednostki z podzestawu typów w hierarchii:
context.Pets.ToList();
Zapytanie zwracające tylko jednostki z pojedynczego typu liścia w hierarchii:
context.Cats.ToList();
W przypadku korzystania z funkcji TPH wszystkie trzy zapytania wysyłają zapytanie tylko do jednej tabeli, ale z różnymi filtrami w kolumnie dyskryminującej:
Funkcja TPH SQL zwraca jednostki wszystkich typów w hierarchii:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
FROM [Animals] AS [a]
Funkcja TPH SQL zwraca jednostki z podzestawu typów w hierarchii:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
FROM [Animals] AS [a]
WHERE [a].[Discriminator] IN (N'Cat', N'Dog')
Funkcja TPH SQL zwraca tylko jednostki z pojedynczego typu liścia w hierarchii:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel]
FROM [Animals] AS [a]
WHERE [a].[Discriminator] = N'Cat'
Wszystkie te zapytania powinny działać dobrze, szczególnie w przypadku odpowiedniego indeksu bazy danych w kolumnie dyskryminującej.
W przypadku korzystania z TPT wszystkie te zapytania wymagają łączenia wielu tabel, ponieważ dane dla dowolnego konkretnego typu są dzielone między wiele tabel:
Funkcja TPT SQL zwraca jednostki wszystkich typów w hierarchii:
SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
WHEN [d].[Id] IS NOT NULL THEN N'Dog'
WHEN [c].[Id] IS NOT NULL THEN N'Cat'
WHEN [h].[Id] IS NOT NULL THEN N'Human'
WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
END AS [Discriminator]
FROM [Animals] AS [a]
LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id]
LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
TPT SQL zwraca jednostki z podzestawu typów w hierarchii:
SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
WHEN [d].[Id] IS NOT NULL THEN N'Dog'
WHEN [c].[Id] IS NOT NULL THEN N'Cat'
END AS [Discriminator]
FROM [Animals] AS [a]
INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
TPT SQL zwraca tylko jednostki z jednego typu liścia w hierarchii:
SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel]
FROM [Animals] AS [a]
INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
Uwaga
Program EF Core używa "syntezy dyskryminującej", aby określić, z której tabeli pochodzą dane, a tym samym prawidłowy typ do użycia. Działa to, ponieważ funkcja LEFT JOIN zwraca wartości null dla kolumny identyfikatora zależnego (tabele podrzędne), które nie są poprawnym typem. Więc dla psa, [d].[Id]
będzie non-null, a wszystkie pozostałe (betonowe) identyfikatory będą mieć wartość null.
Wszystkie te zapytania mogą mieć problemy z wydajnością spowodowane sprzężeniami tabeli. Dlatego TPT nigdy nie jest dobrym wyborem dla wydajności zapytań.
Funkcja TPC poprawia przepływ pracy dla wszystkich tych zapytań, ponieważ liczba tabel, które należy wykonać zapytania, jest ograniczona. Ponadto wyniki z każdej tabeli są łączone przy użyciu metody UNION ALL
, która może być znacznie szybsza niż sprzężenie tabeli, ponieważ nie musi wykonywać żadnych dopasowań między wierszami ani deduplikować wierszy.
Funkcja TPC SQL zwraca jednostki wszystkich typów w hierarchii:
SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
FROM [FarmAnimals] AS [f]
UNION ALL
SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
FROM [Humans] AS [h]
UNION ALL
SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
TPC SQL zwraca jednostki z podzestawu typów w hierarchii:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
TPC SQL zwraca tylko jednostki z pojedynczego typu liścia w hierarchii:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
FROM [Cats] AS [c]
Mimo że funkcja TPC jest lepsza niż TPT dla wszystkich tych zapytań, zapytania TPH są nadal lepsze podczas zwracania wystąpień wielu typów. Jest to jedna z przyczyn, dla których TPH jest domyślną strategią używaną przez program EF Core.
Jak pokazuje język SQL dla zapytania nr 3, funkcja TPC naprawdę wyróżnia się podczas wykonywania zapytań dotyczących jednostek pojedynczego typu liścia. Zapytanie używa tylko jednej tabeli i nie wymaga filtrowania.
Funkcja TPC działa również dobrze podczas zapisywania nowej jednostki, ponieważ wymaga to wstawienia tylko jednego wiersza do pojedynczej tabeli. Dotyczy to również TPH. W przypadku TPT wiersze muszą być wstawione do wielu tabel, co będzie działać mniej dobrze.
To samo jest często prawdziwe w przypadku aktualizacji, chociaż w tym przypadku, jeśli wszystkie aktualizowane kolumny znajdują się w tej samej tabeli, nawet w przypadku TPT, różnica może nie być znacząca.
Zarówno TPT, jak i TPC mogą używać mniej miejsca niż TPH, gdy istnieje wiele podtypów z wieloma właściwościami, które często nie są używane. Dzieje się tak, ponieważ każdy wiersz w tabeli TPH musi przechowywać NULL
dla każdej z tych nieużywanych właściwości. W praktyce rzadko występuje problem, ale warto rozważyć przechowywanie dużych ilości danych z tymi cechami.
Napiwek
Jeśli system bazy danych obsługuje go (e.g. SQL Server), rozważ użycie "rozrzedzonych kolumn" dla kolumn TPH, które będą rzadko wypełniane.
Wybrana strategia mapowania dziedziczenia ma konsekwencje dla sposobu generowania i zarządzania wartościami klucza podstawowego. Klucze w TPH są łatwe, ponieważ każde wystąpienie jednostki jest reprezentowane przez jeden wiersz w jednej tabeli. Można użyć dowolnego rodzaju generowania wartości klucza i nie są potrzebne żadne dodatkowe ograniczenia.
W przypadku strategii TPT zawsze istnieje wiersz w tabeli zamapowany na podstawowy typ hierarchii. W tym wierszu można używać dowolnego rodzaju generowania kluczy, a klucze dla innych tabel są połączone z tą tabelą przy użyciu ograniczeń klucza obcego.
Rzeczy są nieco bardziej skomplikowane dla TPC. Najpierw należy pamiętać, że platforma EF Core wymaga, aby wszystkie jednostki w hierarchii miały unikatową wartość klucza, nawet jeśli jednostki mają różne typy. W związku z tym, korzystając z naszego przykładowego modelu, pies nie może mieć tej samej wartości klucza identyfikatora co Kot. Po drugie, w przeciwieństwie do TPT, nie ma wspólnej tabeli, która może działać jako pojedyncze miejsce, w którym żyją wartości kluczy i można je wygenerować. Oznacza to, że nie można użyć prostej Identity
kolumny.
W przypadku baz danych obsługujących sekwencje wartości kluczy można wygenerować przy użyciu pojedynczej sekwencji przywoływanej w domyślnym ograniczeniu dla każdej tabeli. Jest to strategia używana w tabelach TPC przedstawionych powyżej, gdzie każda tabela ma następujące elementy:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence
to sekwencja bazy danych utworzona przez program EF Core. Ta strategia jest używana domyślnie w przypadku hierarchii TPC podczas korzystania z dostawcy bazy danych EF Core dla programu SQL Server. Dostawcy baz danych dla innych baz danych obsługujących sekwencje powinny mieć podobną wartość domyślną. Inne strategie generowania kluczy, które używają sekwencji, takich jak wzorce Hi-Lo, mogą być również używane z TPC.
Chociaż standardowe kolumny tożsamości nie będą działać z TPC, można użyć kolumn Identity, jeśli każda tabela jest skonfigurowana z odpowiednim inicjatorem i przyrostem, tak aby wartości wygenerowane dla każdej tabeli nigdy nie powodować konfliktu. Na przykład:
modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));
SqLite nie obsługuje sekwencji ani inkrementacji tożsamości, dlatego generowanie wartości klucza całkowitego nie jest obsługiwane w przypadku używania biblioteki SQLite ze strategią TPC. Jednak generowanie po stronie klienta lub globalnie unikatowe klucze — na przykład klucze GUID — są obsługiwane w dowolnej bazie danych, w tym sqlite.
Strategia mapowania TPC tworzy zdenormalizowany schemat SQL — jest to jeden z powodów, dla których niektórzy puryści bazy danych są przeciwni. Rozważmy na przykład kolumnę FavoriteAnimalId
klucza obcego . Wartość w tej kolumnie musi być zgodna z wartością klucza podstawowego niektórych zwierząt. Można to wymusić w bazie danych z prostym ograniczeniem FK podczas korzystania z TPH lub TPT. Na przykład:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Jednak w przypadku korzystania z TPC klucz podstawowy dla zwierzęcia jest przechowywany w tabeli dla konkretnego typu tego zwierzęcia. Na przykład klucz podstawowy kota jest przechowywany w Cats.Id
kolumnie, podczas gdy klucz podstawowy psa jest przechowywany w Dogs.Id
kolumnie itd. Oznacza to, że nie można utworzyć ograniczenia FK dla tej relacji.
W praktyce nie jest to problem, o ile aplikacja nie próbuje wstawić nieprawidłowych danych. Jeśli na przykład wszystkie dane są wstawione przez program EF Core i używają nawigacji do powiązania jednostek, gwarantowana jest, że kolumna FK będzie zawierać prawidłową wartość PK przez cały czas.
Podsumowując, TPC jest dobrą strategią mapowania, która ma być używana, gdy kod będzie w większości wykonywać zapytania dotyczące jednostek pojedynczego typu liścia. Wynika to z tego, że wymagania dotyczące magazynu są mniejsze i nie ma dyskryminującej kolumny, która może wymagać indeksu. Wstawianie i aktualizacje są również wydajne.
To powiedziawszy, TPH jest zwykle w porządku dla większości aplikacji i jest dobrym ustawieniem domyślnym dla szerokiej gamy scenariuszy, więc nie dodawaj złożoności TPC, jeśli nie potrzebujesz. W szczególności jeśli kod będzie w większości wykonywać zapytania dotyczące jednostek wielu typów, takich jak pisanie zapytań względem typu podstawowego, pochylić się w kierunku TPH przez TPC.
Należy użyć TPT tylko wtedy, gdy jest to ograniczone przez czynniki zewnętrzne.
Teraz można dostosować kod szkieletowy podczas odtwarzania modelu EF z bazy danych. Rozpocznij od dodania domyślnych szablonów do projektu:
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
Następnie szablony mogą być dostosowywane i będą automatycznie używane przez dotnet ef dbcontext scaffold
program i Scaffold-DbContext
.
Aby uzyskać więcej informacji, zobacz Niestandardowe szablony inżynierii odwrotnej.
Napiwek
Zespół ef team zademonstrował i omówił szczegółowo szablony inżynierii odwrotnej w odcinku standupu społeczności danych platformy .NET. Podobnie jak w przypadku wszystkich odcinków standup społeczności, możesz obejrzeć odcinek T4 szablonów teraz na YouTube.
Program EF Core używa metadanych "model" do opisania sposobu mapowania typów jednostek aplikacji na bazową bazę danych. Ten model jest tworzony przy użyciu zestawu około 60 "konwencji". Model utworzony za pomocą konwencji można następnie dostosować przy użyciu atrybutów mapowania (np. "adnotacje danych") i/lub wywołań interfejsuDbModelBuilder
API w programie OnModelCreating
.
Począwszy od ef7, aplikacje mogą teraz usuwać lub zastępować dowolną z tych konwencji, a także dodawać nowe konwencje. Konwencje tworzenia modeli to zaawansowany sposób kontrolowania konfiguracji modelu, ale może być złożony i trudny do uzyskania właściwego rozwiązania. W wielu przypadkach można użyć istniejącej konfiguracji modelu przed konwencją, aby łatwo określić wspólną konfigurację właściwości i typów.
Zmiany konwencji używanych przez element DbContext
są wprowadzane przez zastąpienie DbContext.ConfigureConventions
metody . Na przykład:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Napiwek
Aby znaleźć wszystkie wbudowane konwencje tworzenia modeli, poszukaj każdej klasy, która implementuje IConvention interfejs.
Napiwek
Pokazany tutaj kod pochodzi z ModelBuildingConventionsSample.cs.
Czasami jedna z wbudowanych konwencji może nie być odpowiednia dla aplikacji, w tym przypadku można ją usunąć.
Zwykle warto utworzyć indeksy dla kolumn klucza obcego (FK), dlatego istnieje wbudowana konwencja dla tego: ForeignKeyIndexConvention. Patrząc na widok debugowania modelu dla Post
typu jednostki z relacjami z Blog
i Author
, możemy zobaczyć, że dwa indeksy są tworzone — jeden dla BlogId
klucza FK, a drugi dla AuthorId
klucza FK.
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK Index
BlogId (no field, int) Shadow Required FK Index
Navigations:
Author (Author) ToPrincipal Author Inverse: Posts
Blog (Blog) ToPrincipal Blog Inverse: Posts
Keys:
Id PK
Foreign keys:
Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
Indexes:
AuthorId
BlogId
Jednak indeksy mają narzut i, jak zapytano tutaj, może nie zawsze być odpowiednie do utworzenia ich dla wszystkich kolumn FK. Aby to osiągnąć, ForeignKeyIndexConvention
można go usunąć podczas kompilowania modelu:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Patrząc na widok debugowania modelu na Post
razie, widzimy, że indeksy na zestawach FKs nie zostały utworzone:
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK
BlogId (no field, int) Shadow Required FK
Navigations:
Author (Author) ToPrincipal Author Inverse: Posts
Blog (Blog) ToPrincipal Blog Inverse: Posts
Keys:
Id PK
Foreign keys:
Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
W razie potrzeby indeksy mogą być nadal jawnie tworzone dla kolumn kluczy obcych przy użyciu elementu IndexAttribute:
[Index("BlogId")]
public class Post
{
// ...
}
Lub z konfiguracją w pliku OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}
Ponowne przyjrzenie się typowi Post
jednostki zawiera BlogId
indeks, ale nie AuthorId
indeks:
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK
BlogId (no field, int) Shadow Required FK Index
Navigations:
Author (Author) ToPrincipal Author Inverse: Posts
Blog (Blog) ToPrincipal Blog Inverse: Posts
Keys:
Id PK
Foreign keys:
Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
Indexes:
BlogId
Napiwek
Jeśli model nie używa atrybutów mapowania (adnotacji danych) do konfiguracji, wszystkie konwencje kończące AttributeConvention
się można bezpiecznie usunąć, aby przyspieszyć tworzenie modelu.
Usunięcie istniejących konwencji jest początkiem, ale co z dodaniem całkowicie nowych konwencji tworzenia modelu? Program EF7 obsługuje to również!
Strategia mapowania dziedziczenia tabeli na hierarchię wymaga dyskryminującej kolumny w celu określenia, który typ jest reprezentowany w danym wierszu. Domyślnie program EF używa niezwiązanej kolumny ciągu dla dyskryminującego, co gwarantuje, że będzie działać dla dowolnej długości dyskryminującej. Ograniczenie maksymalnej długości ciągów dyskryminujących może jednak zwiększyć wydajność magazynowania i zapytań. Utwórzmy nową konwencję, która to zrobi.
Konwencje tworzenia modeli platformy EF Core są wyzwalane na podstawie zmian wprowadzonych w modelu podczas jego tworzenia. Dzięki temu model jest aktualny w miarę tworzenia jawnej konfiguracji, są stosowane atrybuty mapowania i są uruchamiane inne konwencje. Aby wziąć udział w tym procesie, każda konwencja implementuje jeden lub więcej interfejsów, które określają, kiedy konwencja zostanie wyzwolona. Na przykład konwencja implementowana zostanie wyzwolona IEntityTypeAddedConvention za każdym razem, gdy nowy typ jednostki zostanie dodany do modelu. Podobnie konwencja implementowana zarówno, IForeignKeyAddedConvention jak i IKeyAddedConvention zostanie wyzwolona za każdym razem, gdy do modelu zostanie dodany klucz lub klucz obcy.
Znajomość interfejsów do zaimplementowania może być trudna, ponieważ konfiguracja w danym momencie może zostać zmieniona lub usunięta w późniejszym momencie. Na przykład klucz może zostać utworzony zgodnie z konwencją, ale później zastąpiony podczas jawnego konfigurowania innego klucza.
Zróbmy to nieco bardziej konkretnie, podejmując pierwszą próbę wdrożenia konwencji o długości dyskryminującej:
public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
public void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
if (discriminatorProperty != null
&& discriminatorProperty.ClrType == typeof(string))
{
discriminatorProperty.Builder.HasMaxLength(24);
}
}
}
Ta konwencja implementuje IEntityTypeBaseTypeChangedConventionelement , co oznacza, że zostanie wyzwolony przy każdej zmianie zamapowanej hierarchii dziedziczenia dla typu jednostki. Następnie konwencja znajduje i konfiguruje właściwość dyskryminującą ciąg dla hierarchii.
Ta konwencja jest następnie używana przez wywołanie metody Add
w pliku ConfigureConventions
:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DiscriminatorLengthConvention1());
}
Napiwek
Zamiast bezpośrednio dodawać wystąpienie konwencji, Add
metoda akceptuje fabrykę do tworzenia wystąpień konwencji. Dzięki temu konwencja może używać zależności od wewnętrznego dostawcy usług EF Core. Ponieważ ta konwencja nie ma zależności, parametr dostawcy usług ma nazwę _
, wskazując, że nigdy nie jest używany.
Utworzenie modelu i przyjrzenie się typowi Post
jednostki pokazuje, że ta właściwość działa — właściwość dyskryminująca jest teraz skonfigurowana do maksymalnej długości 24:
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Ale co się stanie, jeśli teraz jawnie skonfigurujemy inną właściwość dyskryminującą? Na przykład:
modelBuilder.Entity<Post>()
.HasDiscriminator<string>("PostTypeDiscriminator")
.HasValue<Post>("Post")
.HasValue<FeaturedPost>("Featured");
Patrząc na widok debugowania modelu, uważamy, że długość dyskryminująca nie jest już skonfigurowana.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw
Wynika to z faktu, że właściwość dyskryminująca skonfigurowana w naszej konwencji została później usunięta po dodaniu niestandardowego dyskryminatora. Moglibyśmy spróbować rozwiązać ten problem, wdrażając inny interfejs w naszej konwencji, aby reagować na zmiany dyskryminujące, ale ustalenie, który interfejs do zaimplementowania nie jest łatwy.
Na szczęście istnieje inny sposób podejścia, który znacznie ułatwia kwestie. Wiele czasu nie ma znaczenia, jak wygląda model podczas kompilowania, o ile ostateczny model jest poprawny. Ponadto konfiguracja, którą chcemy zastosować, często nie musi wyzwalać innych konwencji, aby reagować. W związku z tym nasza konwencja może implementować IModelFinalizingConvention. Konwencje finalizacji modelu są uruchamiane po zakończeniu wszystkich innych kompilacji modeli, a więc mają dostęp do końcowego stanu modelu. Konwencja finalizowania modelu zwykle iteruje cały model konfigurujący elementy modelu w miarę jego działania. W tym przypadku znajdziemy więc każdy dyskryminator w modelu i skonfigurujemy go:
public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
.Where(entityType => entityType.BaseType == null))
{
var discriminatorProperty = entityType.FindDiscriminatorProperty();
if (discriminatorProperty != null
&& discriminatorProperty.ClrType == typeof(string))
{
discriminatorProperty.Builder.HasMaxLength(24);
}
}
}
}
Po utworzeniu modelu z tą nową konwencją okazuje się, że długość dyskryminująca jest teraz poprawnie skonfigurowana, mimo że została ona dostosowana:
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Po prostu dla zabawy, przejdźmy o krok dalej i skonfigurujmy maksymalną długość najdłuższej wartości dyskryminującej.
public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
.Where(entityType => entityType.BaseType == null))
{
var discriminatorProperty = entityType.FindDiscriminatorProperty();
if (discriminatorProperty != null
&& discriminatorProperty.ClrType == typeof(string))
{
var maxDiscriminatorValueLength =
entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();
discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
}
}
}
}
Teraz maksymalna długość kolumny dyskryminującej wynosi 8, czyli długość "Polecane", najdłuższa wartość dyskryminująca w użyciu.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
Napiwek
Być może zastanawiasz się, czy konwencja powinna również utworzyć indeks dla kolumny dyskryminującej. Istnieje dyskusja na temat tego tematu w witrynie GitHub. Krótka odpowiedź polega na tym, że czasami indeks może być przydatny, ale przez większość czasu prawdopodobnie nie będzie. Dlatego najlepiej jest utworzyć odpowiednie indeksy w razie potrzeby, a nie mieć konwencji, aby to robić zawsze. Ale jeśli nie zgadzasz się z tym, powyższe konwencje można łatwo zmodyfikować w celu utworzenia indeksu.
Przyjrzyjmy się innemu przykładowi, w którym można użyć konwencji finalizowania — tym razem ustawiając domyślną maksymalną długość dowolnej właściwości ciągu, która jest żądana w usłudze GitHub. Konwencja wygląda podobnie do poprzedniego przykładu:
public class MaxStringLengthConvention : IModelFinalizingConvention
{
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var property in modelBuilder.Metadata.GetEntityTypes()
.SelectMany(
entityType => entityType.GetDeclaredProperties()
.Where(
property => property.ClrType == typeof(string))))
{
property.Builder.HasMaxLength(512);
}
}
}
Ta konwencja jest dość prosta. Znajduje on każdą właściwość ciągu w modelu i ustawia maksymalną długość na 512. Patrząc w widoku debugowania we właściwościach Post
elementu , widzimy, że wszystkie właściwości ciągu mają teraz maksymalną długość 512.
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK Index
BlogId (no field, int) Shadow Required FK Index
Content (string) Required MaxLength(512)
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
PublishedOn (DateTime) Required
Title (string) Required MaxLength(512)
Content
Ale właściwość powinna prawdopodobnie zezwalać na więcej niż 512 znaków, lub wszystkie nasze posty będą dość krótkie! Można to zrobić bez zmiany konwencji, jawnie konfigurując maksymalną długość tylko dla tej właściwości przy użyciu atrybutu mapowania:
[MaxLength(4000)]
public string Content { get; set; }
Lub za pomocą kodu w pliku OnModelCreating
:
modelBuilder.Entity<Post>()
.Property(post => post.Content)
.HasMaxLength(4000);
Teraz wszystkie właściwości mają maksymalną długość 512, z wyjątkiem Content
jawnie skonfigurowanej z 4000:
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK Index
BlogId (no field, int) Shadow Required FK Index
Content (string) Required MaxLength(4000)
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
PublishedOn (DateTime) Required
Title (string) Required MaxLength(512)
Dlaczego więc nasza konwencja nie zastąpi jawnie skonfigurowanej maksymalnej długości? Odpowiedź brzmi, że program EF Core śledzi, jak każda konfiguracja została wykonana. Jest to reprezentowane przez wyliczenie ConfigurationSource . Różne rodzaje konfiguracji to:
Explicit
: Element modelu został jawnie skonfigurowany w programie OnModelCreating
DataAnnotation
: Element modelu został skonfigurowany przy użyciu atrybutu mapowania (adnotacji danych) w typie CLRConvention
: Element modelu został skonfigurowany zgodnie z konwencją tworzenia modeluKonwencje nigdy nie zastępują konfiguracji oznaczonej jako DataAnnotation
lub Explicit
. Jest to osiągane przy użyciu "konstruktora konwencji", IConventionPropertyBuilderna przykład , który jest uzyskiwany z Builder właściwości . Na przykład:
property.Builder.HasMaxLength(512);
Wywołanie metody HasMaxLength
na konstruktorze konwencji spowoduje ustawienie maksymalnej długości tylko wtedy, gdy nie został jeszcze skonfigurowany przez atrybut mapowania lub w .OnModelCreating
Metody konstruktora takie jak ten mają również drugi parametr: fromDataAnnotation
. Ustaw tę wartość na true
wartość , jeśli konwencja tworzy konfigurację w imieniu atrybutu mapowania. Na przykład:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
Spowoduje to ustawienie ConfigurationSource
wartości na DataAnnotation
, co oznacza, że wartość może być teraz zastępowana przez jawne mapowanie na OnModelCreating
, ale nie przez konwencje atrybutów niemapowania.
Na koniec, zanim opuścimy ten przykład, co się stanie, jeśli używamy obu elementów MaxStringLengthConvention
i DiscriminatorLengthConvention3
w tym samym czasie? Odpowiedź polega na tym, że zależy od kolejności ich dodania, ponieważ konwencje finalizacji modelu są uruchamiane w kolejności ich dodawania. Więc jeśli MaxStringLengthConvention
zostanie dodany ostatnio, będzie on uruchamiany jako ostatni, i ustawi maksymalną długość właściwości dyskryminującej na 512. W związku z tym w tym przypadku lepiej jest dodać DiscriminatorLengthConvention3
ostatnio, aby można było zastąpić domyślną maksymalną długość tylko właściwości dyskryminujących, pozostawiając wszystkie inne właściwości ciągu jako 512.
Czasami zamiast całkowicie usuwać istniejącą konwencję, zamiast tego chcemy zastąpić ją konwencją, która zasadniczo robi to samo, ale ze zmienionym zachowaniem. Jest to przydatne, ponieważ istniejąca konwencja będzie już implementować interfejsy, których potrzebuje, tak aby była odpowiednio wyzwalana.
Program EF Core mapuje wszystkie publiczne właściwości odczytu i zapisu według konwencji. Może to nie być odpowiednie dla sposobu definiowania typów jednostek. Aby to zmienić, możemy zastąpić PropertyDiscoveryConvention
element własnym implementacją, która nie mapuje żadnej właściwości, chyba że jest jawnie zamapowana lub OnModelCreating
oznaczona nowym atrybutem o nazwie Persist
:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}
Oto nowa konwencja:
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
Napiwek
Podczas zastępowania wbudowanej konwencji nowa implementacja konwencji powinna dziedziczyć z istniejącej klasy konwencji. Należy pamiętać, że niektóre konwencje mają implementacje specyficzne dla relacyjnych lub dostawcy, w tym przypadku nowa implementacja konwencji powinna dziedziczyć z najbardziej konkretnej istniejącej klasy konwencji dla dostawcy bazy danych w użyciu.
Konwencja jest następnie rejestrowana przy użyciu metody w pliku Replace
ConfigureConventions
:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}
Napiwek
Jest to przypadek, w którym istniejąca konwencja ma zależności reprezentowane przez ProviderConventionSetBuilderDependencies
obiekt zależności. Są one uzyskiwane od wewnętrznego dostawcy usług przy użyciu GetRequiredService
i przekazywane do konstruktora konwencji.
Ta konwencja działa przez pobranie wszystkich czytelnych właściwości i pól z danego typu jednostki. Jeśli element członkowski jest przypisywany za pomocą [Persist]
elementu , jest mapowany przez wywołanie metody :
entityTypeBuilder.Property(memberInfo);
Z drugiej strony, jeśli element członkowski jest właściwością, która w przeciwnym razie zostałaby zamapowana, zostanie wykluczona z modelu przy użyciu:
entityTypeBuilder.Ignore(propertyInfo.Name);
Należy zauważyć, że ta konwencja umożliwia mapowanie pól (oprócz właściwości), o ile są one oznaczone znakiem [Persist]
. Oznacza to, że możemy używać pól prywatnych jako ukrytych kluczy w modelu.
Rozważmy na przykład następujące typy jednostek:
public class LaundryBasket
{
[Persist]
[Key]
private readonly int _id;
[Persist]
public int TenantId { get; init; }
public bool IsClean { get; set; }
public List<Garment> Garments { get; } = new();
}
public class Garment
{
public Garment(string name, string color)
{
Name = name;
Color = color;
}
[Persist]
[Key]
private readonly int _id;
[Persist]
public int TenantId { get; init; }
[Persist]
public string Name { get; }
[Persist]
public string Color { get; }
public bool IsClean { get; set; }
public LaundryBasket? Basket { get; set; }
}
Model utworzony na podstawie tych typów jednostek to:
Model:
EntityType: Garment
Properties:
_id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
Basket_id (no field, int?) Shadow FK Index
Color (string) Required
Name (string) Required
TenantId (int) Required
Navigations:
Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
Keys:
_id PK
Foreign keys:
Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
Indexes:
Basket_id
EntityType: LaundryBasket
Properties:
_id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
TenantId (int) Required
Navigations:
Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
Keys:
_id PK
Należy zauważyć, że normalnie, zostałyby zamapowane, IsClean
ale ponieważ nie jest on oznaczony [Persist]
(prawdopodobnie ponieważ czystość nie jest trwałą właściwością prania), jest teraz traktowany jako właściwość niemapowana.
Napiwek
Nie można zaimplementować tej konwencji jako konwencji finalizowania modelu, ponieważ mapowanie właściwości wyzwala wiele innych konwencji do uruchomienia w celu dalszego konfigurowania zamapowanej właściwości.
Domyślnie program EF Core generuje polecenia wstawiania, aktualizowania i usuwania, które działają bezpośrednio z tabelami lub widokami aktualizowalnymi. Program EF7 wprowadza obsługę mapowania tych poleceń na procedury składowane.
Napiwek
Program EF Core zawsze obsługiwał wykonywanie zapytań za pośrednictwem procedur składowanych. Nowa obsługa w programie EF7 jest jawnie używana w procedurach składowanych w przypadku wstawiania, aktualizacji i usuwania.
Ważne
Obsługa mapowania procedur składowanych nie oznacza, że zalecane są procedury składowane.
Procedury składowane są mapowane przy OnModelCreating
użyciu metod InsertUsingStoredProcedure
, UpdateUsingStoredProcedure
i DeleteUsingStoredProcedure
. Aby na przykład zamapować procedury składowane dla Person
typu jednostki:
modelBuilder.Entity<Person>()
.InsertUsingStoredProcedure(
"People_Insert",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter(a => a.Name);
storedProcedureBuilder.HasResultColumn(a => a.Id);
})
.UpdateUsingStoredProcedure(
"People_Update",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
})
.DeleteUsingStoredProcedure(
"People_Delete",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
Ta konfiguracja jest mapowana na następujące procedury składowane podczas korzystania z programu SQL Server:
W przypadku wstawiania
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
W przypadku aktualizacji
CREATE PROCEDURE [dbo].[People_Update]
@Id [int],
@Name_Original [nvarchar](max),
@Name [nvarchar](max)
AS
BEGIN
UPDATE [People] SET [Name] = @Name
WHERE [Id] = @Id AND [Name] = @Name_Original
SELECT @@ROWCOUNT
END
W przypadku usuwania
CREATE PROCEDURE [dbo].[People_Delete]
@Id [int],
@Name_Original [nvarchar](max)
AS
BEGIN
DELETE FROM [People]
OUTPUT 1
WHERE [Id] = @Id AND [Name] = @Name_Original;
END
Napiwek
Procedury składowane nie muszą być używane dla każdego typu w modelu ani dla wszystkich operacji na danym typie. Jeśli na przykład określono tylko DeleteUsingStoredProcedure
dla danego typu, program EF Core wygeneruje program SQL normalnie dla operacji wstawiania i aktualizacji i używa tylko procedury składowanej do usuwania.
Pierwszym argumentem przekazywanym do każdej metody jest nazwa procedury składowanej. Można to pominąć, w takim przypadku program EF Core będzie używać nazwy tabeli dołączonej do "_Insert", "_Update" lub "_Delete". Dlatego w powyższym przykładzie, ponieważ tabela nosi nazwę "People", nazwy procedur składowanej można usunąć bez zmian w funkcjonalności.
Drugi argument to konstruktor służący do konfigurowania danych wejściowych i wyjściowych procedury składowanej, w tym parametrów, zwracanych wartości i kolumn wyników.
Parametry należy dodać do konstruktora w takiej samej kolejności, jak w definicji procedury składowanej.
Uwaga
Parametry mogą być nazwane, ale program EF Core zawsze wywołuje procedury składowane przy użyciu argumentów pozycyjnych, a nie nazwanych argumentów. Zagłosuj na ustawienie Zezwalaj na konfigurowanie mapowania sproc w celu użycia nazw parametrów do wywołania , jeśli wywołanie według nazwy jest czymś, co cię interesuje.
Pierwszy argument dla każdej metody konstruktora parametrów określa właściwość w modelu, do którego jest powiązany parametr. Może to być wyrażenie lambda:
storedProcedureBuilder.HasParameter(a => a.Name);
Lub ciąg, który jest szczególnie przydatny podczas mapowania właściwości cienia:
storedProcedureBuilder.HasParameter("Name");
Parametry są domyślnie skonfigurowane dla "danych wejściowych". Parametry "Output" lub "input/output" można skonfigurować przy użyciu konstruktora zagnieżdżonego. Na przykład:
storedProcedureBuilder.HasParameter(
document => document.RetrievedOn,
parameterBuilder => parameterBuilder.IsOutput());
Istnieją trzy różne metody konstruktora dla różnych odmian parametrów:
HasParameter
określa normalny parametr powiązany z bieżącą wartością danej właściwości.HasOriginalValueParameter
określa parametr powiązany z oryginalną wartością danej właściwości. Oryginalna wartość to wartość, którą właściwość miała podczas wykonywania zapytania z bazy danych, jeśli jest znana. Jeśli ta wartość nie jest znana, zamiast tego zostanie użyta bieżąca wartość. Oryginalne parametry wartości są przydatne w przypadku tokenów współbieżności.HasRowsAffectedParameter
określa parametr używany do zwracania liczby wierszy, których dotyczy procedura składowana.Napiwek
Oryginalne parametry wartości muszą być używane dla wartości klucza w procedurach składowanych "update" i "delete". Dzięki temu prawidłowy wiersz zostanie zaktualizowany w przyszłych wersjach programu EF Core, które obsługują modyfikowalne wartości kluczy.
Program EF Core obsługuje trzy mechanizmy zwracania wartości z procedur składowanych:
HasResultColumn
.HasRowsAffectedReturnValue
.Wartości zwracane z procedur składowanych są często używane dla wygenerowanych, domyślnych lub obliczonych wartości, takich jak klucz Identity
lub obliczona kolumna. Na przykład następująca konfiguracja określa cztery kolumny wyników:
entityTypeBuilder.InsertUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter(document => document.Title);
storedProcedureBuilder.HasResultColumn(document => document.Id);
storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn);
storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn);
storedProcedureBuilder.HasResultColumn(document => document.RowVersion);
});
Są one używane do zwracania:
Id
właściwości .FirstRecordedOn
właściwości .RetrievedOn
właściwości .rowversion
token współbieżności dla RowVersion
właściwości .Ta konfiguracja jest mapowana na następującą procedurę składowaną podczas korzystania z programu SQL Server:
CREATE PROCEDURE [dbo].[Documents_Insert]
@Title [nvarchar](max)
AS
BEGIN
INSERT INTO [Documents] ([Title])
OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion]
VALUES (@Title);
END
Optymistyczna współbieżność działa tak samo jak w przypadku procedur składowanych, jak w przypadku braku. Procedura składowana powinna:
WHERE
klauzuli , aby upewnić się, że wiersz jest aktualizowany tylko wtedy, gdy ma prawidłowy token. Wartość używana dla tokenu współbieżności jest zazwyczaj, ale nie musi być oryginalną wartością właściwości tokenu współbieżności.DbUpdateConcurrencyException
wartość, jeśli wartości nie są zgodne.Na przykład następująca procedura składowana programu SQL Server używa tokenu automatycznej rowversion
współbieżności:
CREATE PROCEDURE [dbo].[Documents_Update]
@Id [int],
@RowVersion_Original [rowversion],
@Title [nvarchar](max),
@RowVersion [rowversion] OUT
AS
BEGIN
DECLARE @TempTable table ([RowVersion] varbinary(8));
UPDATE [Documents] SET
[Title] = @Title
OUTPUT INSERTED.[RowVersion] INTO @TempTable
WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original
SELECT @@ROWCOUNT;
SELECT @RowVersion = [RowVersion] FROM @TempTable;
END
Jest to skonfigurowane w programie EF Core przy użyciu:
.UpdateUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(document => document.Id);
storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion);
storedProcedureBuilder.HasParameter(document => document.Title);
storedProcedureBuilder.HasParameter(document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput());
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
Zwróć uwagę, że:
RowVersion
współbieżności.WHERE
, aby upewnić się, że wiersz jest aktualizowany tylko wtedy, gdy oryginalna wartość jest zgodna RowVersion
.RowVersion
jest wstawiana do tabeli tymczasowej.@@ROWCOUNT
) i wygenerowana RowVersion
wartość.Program EF Core wymaga, aby procedury składowane postępowały zgodnie z układem tabeli dla typów w hierarchii. To oznacza, że:
Uwaga
Jeśli używasz jednej procedury składowanej dla konkretnego typu niezależnie od strategii mapowania jest czymś, co cię interesuje, zagłosuj na pomoc techniczną przy użyciu pojedynczego typu sproc na konkretny typ niezależnie od strategii mapowania dziedziczenia.
Konfiguracja procedur składowanych dla typów należących do użytkownika odbywa się w zagnieżdżonym konstruktorze typów. Na przykład:
modelBuilder.Entity<Person>(
entityTypeBuilder =>
{
entityTypeBuilder.OwnsOne(
author => author.Contact,
ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToTable("Contacts");
ownedNavigationBuilder
.InsertUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter("PersonId");
storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
})
.UpdateUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter("PersonId");
storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
storedProcedureBuilder.HasRowsAffectedResultColumn();
})
.DeleteUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter("PersonId");
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
});
Uwaga
Obecnie procedury składowane dotyczące wstawiania, aktualizowania i usuwania muszą być mapowane tylko na osobne tabele. Oznacza to, że typ własności nie może być reprezentowany przez kolumny w tabeli właściciela. Zagłosuj na dodanie obsługi podziału "tabeli" do mapowania sproc CUD, jeśli jest to ograniczenie, które chcesz zobaczyć usunięte.
Konfigurację procedur składowanych wiele do wielu jednostek sprzężenia można wykonać w ramach konfiguracji wiele-do-wielu. Na przykład:
modelBuilder.Entity<Book>(
entityTypeBuilder =>
{
entityTypeBuilder
.HasMany(document => document.Authors)
.WithMany(author => author.PublishedWorks)
.UsingEntity<Dictionary<string, object>>(
"BookPerson",
builder => builder.HasOne<Person>().WithMany().OnDelete(DeleteBehavior.Cascade),
builder => builder.HasOne<Book>().WithMany().OnDelete(DeleteBehavior.ClientCascade),
joinTypeBuilder =>
{
joinTypeBuilder
.InsertUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter("AuthorsId");
storedProcedureBuilder.HasParameter("PublishedWorksId");
})
.DeleteUsingStoredProcedure(
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter("AuthorsId");
storedProcedureBuilder.HasOriginalValueParameter("PublishedWorksId");
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
});
});
Przechwytniki ef Core umożliwiają przechwytywanie, modyfikowanie i/lub pomijanie operacji platformy EF Core. Program EF Core obejmuje również tradycyjne zdarzenia i rejestrowanie platformy .NET.
Program EF7 oferuje następujące ulepszenia przechwytywania:
DbUpdateConcurrencyException
)DbConnection
przez program EF CoreDbCommand
po zainicjowaniuPonadto program EF7 obejmuje nowe tradycyjne zdarzenia platformy .NET dla:
DetectChanges
. przechwytywanie)W poniższych sekcjach przedstawiono kilka przykładów użycia tych nowych funkcji przechwytywania.
Napiwek
Pokazany tutaj kod pochodzi z SimpleMaterializationSample.cs.
Nowa IMaterializationInterceptor obsługuje przechwytywanie przed utworzeniem i po utworzeniu wystąpienia jednostki oraz przed i po zainicjowaniu właściwości tego wystąpienia. Przechwytujący może zmienić lub zamienić wystąpienie jednostki w każdym punkcie. Pozwala:
Załóżmy na przykład, że chcemy śledzić czas pobierania jednostki z bazy danych, być może dlatego może być wyświetlany użytkownikowi edytując dane. Aby to osiągnąć, najpierw zdefiniujemy interfejs:
public interface IHasRetrieved
{
DateTime Retrieved { get; set; }
}
Używanie interfejsu jest powszechne w przypadku przechwytywania, ponieważ umożliwia to tej samej przechwytywaniu pracę z wieloma różnymi typami jednostek. Na przykład:
public class Customer : IHasRetrieved
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? PhoneNumber { get; set; }
[NotMapped]
public DateTime Retrieved { get; set; }
}
Zwróć uwagę, że [NotMapped]
atrybut służy do wskazywania, że ta właściwość jest używana tylko podczas pracy z jednostką i nie powinna być utrwalana w bazie danych.
Przechwytator musi następnie zaimplementować odpowiednią metodę z IMaterializationInterceptor
i ustawić pobrany czas:
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
Wystąpienie tego przechwytywania jest rejestrowane podczas konfigurowania elementu DbContext
:
public class CustomerContext : DbContext
{
private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();
public DbSet<Customer> Customers
=> Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_setRetrievedInterceptor)
.UseSqlite("Data Source = customers.db");
}
Napiwek
Ten przechwytywanie jest bezstanowe, co jest wspólne, więc pojedyncze wystąpienie jest tworzone i współużytkowane między wszystkimi DbContext
wystąpieniami.
Teraz za każdym razem, gdy zostanie Customer
wykonane zapytanie z bazy danych, Retrieved
właściwość zostanie ustawiona automatycznie. Na przykład:
await using (var context = new CustomerContext())
{
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}
Generuje dane wyjściowe:
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
Napiwek
Pokazany tutaj kod pochodzi z InjectLoggerSample.cs.
Program EF Core ma już wbudowaną obsługę wstrzykiwania niektórych specjalnych usług do wystąpień kontekstowych; na przykład zobacz Ładowanie z opóźnieniem bez serwerów proxy, które działa przez wstrzyknięcie ILazyLoader
usługi.
Element IMaterializationInterceptor
może służyć do uogólninia tej usługi w dowolnej usłudze. W poniższym przykładzie pokazano, jak wstrzyknąć element ILogger do jednostek, tak aby mogły wykonywać własne rejestrowanie.
Uwaga
Wstrzykiwanie usług do jednostek łączy te typy jednostek z wstrzykniętymi usługami, które niektórzy użytkownicy uważają za antywzór.
Tak jak poprzednio, interfejs służy do definiowania, co można zrobić.
public interface IHasLogger
{
ILogger? Logger { get; set; }
}
Typy jednostek, które będą rejestrować, muszą implementować ten interfejs. Na przykład:
public class Customer : IHasLogger
{
private string? _phoneNumber;
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? PhoneNumber
{
get => _phoneNumber;
set
{
Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");
_phoneNumber = value;
}
}
[NotMapped]
public ILogger? Logger { get; set; }
}
Tym razem przechwytywanie musi zaimplementować IMaterializationInterceptor.InitializedInstance
element , który jest wywoływany po utworzeniu każdego wystąpienia jednostki, a jego wartości właściwości zostały zainicjowane. Przechwytujący uzyskuje element ILogger
z kontekstu i inicjuje IHasLogger.Logger
go:
public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
private ILogger? _logger;
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasLogger hasLogger)
{
_logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
hasLogger.Logger = _logger;
}
return instance;
}
}
Tym razem jest używane nowe wystąpienie przechwytywania dla każdego DbContext
wystąpienia, ponieważ ILogger
uzyskane może ulec zmianie na DbContext
wystąpienie, a ILogger
element jest buforowany na przechwytnika:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
Teraz za każdym razem, gdy Customer.PhoneNumber
ta zmiana zostanie zmieniona, ta zmiana zostanie zarejestrowana w dzienniku aplikacji. Na przykład:
info: CustomersLogger[1]
Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.
Napiwek
Pokazany tutaj kod pochodzi z QueryInterceptionSample.cs.
Program EF Core korzysta z zapytań LINQ platformy .NET. Zwykle wiąże się to z użyciem kompilatora języka C#, VB lub F# w celu utworzenia drzewa wyrażeń, które następnie jest tłumaczone przez program EF Core na odpowiedni język SQL. Rozważmy na przykład metodę zwracającą stronę klientów:
Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
using var context = new CustomerContext();
return context.Customers
.OrderBy(e => EF.Property<object>(e, sortProperty))
.Skip(page * 20).Take(20).ToListAsync();
}
Napiwek
To zapytanie używa EF.Property metody , aby określić właściwość do sortowania. Dzięki temu aplikacja może dynamicznie przekazywać nazwę właściwości, umożliwiając sortowanie według dowolnej właściwości typu jednostki. Należy pamiętać, że sortowanie według kolumn nieindeksowanych może być powolne.
Będzie to działać prawidłowo, o ile właściwość używana do sortowania zawsze zwraca stabilną kolejność. Ale to nie zawsze może być tak. Na przykład powyższe zapytanie LINQ generuje następujące polecenie w sqlite podczas zamawiania według :Customer.City
SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0
Jeśli istnieje wielu klientów z tym samym City
elementem , kolejność tego zapytania nie jest stabilna. Może to prowadzić do braku lub zduplikowania wyników jako stron użytkownika za pośrednictwem danych.
Typowym sposobem rozwiązania tego problemu jest wykonanie pomocniczego sortowania według klucza podstawowego. Jednak zamiast ręcznie dodawać je do każdego zapytania, program EF7 umożliwia przechwycenie drzewa wyrażeń zapytania, w którym można dynamicznie dodać kolejność pomocniczą. Aby to ułatwić, ponownie użyjemy interfejsu, tym razem dla każdej jednostki, która ma klucz podstawowy liczby całkowitej:
public interface IHasIntKey
{
int Id { get; }
}
Ten interfejs jest implementowany przez interesujące typy jednostek:
public class Customer : IHasIntKey
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? City { get; set; }
public string? PhoneNumber { get; set; }
}
Następnie potrzebujemy przechwytnika, który implementuje IQueryExpressionInterceptor
public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
=> new KeyOrderingExpressionVisitor().Visit(queryExpression);
private class KeyOrderingExpressionVisitor : ExpressionVisitor
{
private static readonly MethodInfo ThenByMethod
= typeof(Queryable).GetMethods()
.Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);
protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
{
var methodInfo = methodCallExpression!.Method;
if (methodInfo.DeclaringType == typeof(Queryable)
&& methodInfo.Name == nameof(Queryable.OrderBy)
&& methodInfo.GetParameters().Length == 2)
{
var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
{
var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
var entityParameterExpression = lambdaExpression.Parameters[0];
return Expression.Call(
ThenByMethod.MakeGenericMethod(
sourceType,
typeof(int)),
base.VisitMethodCall(methodCallExpression),
Expression.Lambda(
typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
entityParameterExpression));
}
}
return base.VisitMethodCall(methodCallExpression);
}
}
}
To prawdopodobnie wygląda dość skomplikowane - i to jest! Praca z drzewami wyrażeń zwykle nie jest łatwa. Przyjrzyjmy się temu, co się dzieje:
Zasadniczo przechwytujący hermetyzuje ExpressionVisitorelement . Odwiedzający zastąpi metodę VisitMethodCall, która będzie wywoływana za każdym razem, gdy w drzewie wyrażeń zapytania występuje wywołanie metody.
Odwiedzający sprawdza, czy jest to wywołanie OrderBy metody, która nas interesuje.
Jeśli tak jest, odwiedzający dalej sprawdza, czy wywołanie metody ogólnej jest dla typu, który implementuje nasz IHasIntKey
interfejs.
W tym momencie wiemy, że wywołanie metody ma postać OrderBy(e => ...)
. Wyodrębniamy wyrażenie lambda z tego wywołania i pobieramy parametr używany w tym wyrażeniu e
— czyli .
Teraz tworzymy nowe MethodCallExpression przy użyciu metody konstruktora Expression.Call . W takim przypadku wywoływana metoda to ThenBy(e => e.Id)
. Tworzymy to przy użyciu parametru wyodrębnionego powyżej i dostępu właściwości do Id
właściwości interfejsu IHasIntKey
.
Dane wejściowe do tego wywołania są oryginalne OrderBy(e => ...)
, a więc wynik końcowy jest wyrażeniem dla OrderBy(e => ...).ThenBy(e => e.Id)
elementu .
To zmodyfikowane wyrażenie jest zwracane przez odwiedzających, co oznacza, że zapytanie LINQ zostało odpowiednio zmodyfikowane w celu dołączenia wywołania ThenBy
.
Program EF Core kontynuuje i kompiluje to wyrażenie zapytania do odpowiedniego języka SQL dla używanej bazy danych.
Ten przechwytywanie jest rejestrowane w taki sam sposób, jak w przypadku pierwszego przykładu. GetPageOfCustomers
Wykonanie powoduje teraz wygenerowanie następującego kodu SQL:
SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0
Teraz zawsze będzie to generować stabilne zamówienie, nawet jeśli istnieje wielu klientów z tym samym City
.
Uff! Jest to dużo kodu, aby wprowadzić prostą zmianę w zapytaniu. Co gorsza, może nawet nie działać dla wszystkich zapytań. Notorycznie trudno jest napisać odwiedzającym wyrażenie, które rozpoznaje wszystkie kształty zapytania, których powinien, i żaden z tych, których nie powinien. Na przykład prawdopodobnie nie zadziała to, jeśli kolejność jest wykonywana w podzapytaniu.
To prowadzi nas do krytycznego punktu na temat przechwytujących - zawsze pytaj siebie, czy istnieje łatwiejszy sposób robienia tego, co chcesz. Przechwytniki są potężne, ale łatwo jest coś złego. Są one, jak mówi mówi, łatwy sposób, aby strzelić się w stopę.
Załóżmy na przykład, że zamiast tego zmieniliśmy naszą GetPageOfCustomers
metodę w następujący sposób:
Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
using var context = new CustomerContext();
return context.Customers
.OrderBy(e => EF.Property<object>(e, sortProperty))
.ThenBy(e => e.Id)
.Skip(page * 20).Take(20).ToListAsync();
}
W takim przypadku element ThenBy
jest po prostu dodawany do zapytania. Tak, może być konieczne wykonanie go oddzielnie dla każdego zapytania, ale jest proste, łatwe do zrozumienia i zawsze będzie działać.
Napiwek
Pokazany tutaj kod pochodzi z OptimisticConcurrencyInterceptionSample.cs.
Program EF Core obsługuje optymistyczny wzorzec współbieżności, sprawdzając, czy liczba wierszy, których dotyczy aktualizacja lub usunięcie, jest taka sama jak liczba wierszy, których oczekuje się, że będzie to miało wpływ. Jest to często powiązane z tokenem współbieżności; oznacza to, że wartość kolumny, która będzie zgodna tylko z oczekiwaną wartością, jeśli wiersz nie został zaktualizowany od czasu odczytania oczekiwanej wartości.
Ef sygnalizuje naruszenie optymistycznej współbieżności, zgłaszając element DbUpdateConcurrencyException. W programie EF7 istnieją ISaveChangesInterceptor nowe metody ThrowingConcurrencyException
, ThrowingConcurrencyExceptionAsync
które są wywoływane przed zgłoszeniem DbUpdateConcurrencyException
. Te punkty przechwytywania umożliwiają pomijanie wyjątku, prawdopodobnie w połączeniu ze zmianami asynchronicznych bazy danych w celu rozwiązania naruszenia.
Jeśli na przykład dwa żądania spróbują usunąć tę samą jednostkę w niemal tym samym czasie, drugie usunięcie może zakończyć się niepowodzeniem, ponieważ wiersz w bazie danych już nie istnieje. Może to być wynik końcowy, ponieważ mimo to jednostka została usunięta. Poniższy przechwytujący pokazuje, jak można to zrobić:
public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
public InterceptionResult ThrowingConcurrencyException(
ConcurrencyExceptionEventData eventData,
InterceptionResult result)
{
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
{
Console.WriteLine("Suppressing Concurrency violation for command:");
Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);
return InterceptionResult.Suppress();
}
return result;
}
public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
ConcurrencyExceptionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
=> new(ThrowingConcurrencyException(eventData, result));
}
Istnieje kilka rzeczy, które warto zauważyć o tym przechwytywaniu:
SaveChanges
lub SaveChangesAsync
. Jeśli jednak cały kod aplikacji jest asynchroniczny, należy zaimplementować tylko ThrowingConcurrencyExceptionAsync
te elementy. Podobnie, jeśli aplikacja nigdy nie używa metod asynchronicznych bazy danych, należy zaimplementować tylko ThrowingConcurrencyException
te metody. Zwykle dotyczy to wszystkich przechwytujących metod synchronizacji i asynchronicznych. (Warto zaimplementować metodę, która nie jest używana do zgłaszania przez aplikację, na wypadek, gdy część kodu synchronizacji/asynchronicznego wkradnie się).InterceptionResult.Suppress()
informuje platformę EF Core, aby pominąć akcję, która miała na celu podjęcie — w tym przypadku zgłasza błąd DbUpdateConcurrencyException
. Ta możliwość zmiany zachowania platformy EF Core, a nie tylko obserwowania tego, co robi program EF Core, jest jedną z najbardziej zaawansowanych funkcji przechwytywania.Napiwek
Pokazany tutaj kod pochodzi z LazyConnectionStringSample.cs.
Parametry połączenia są często statycznymi elementami zawartości odczytywanym z pliku konfiguracji. Można je łatwo przekazać do UseSqlServer
lub podobnego podczas konfigurowania obiektu DbContext
. Jednak czasami parametry połączenia może ulec zmianie dla każdego wystąpienia kontekstu. Na przykład każda dzierżawa w systemie z wieloma dzierżawami może mieć inne parametry połączenia.
Program EF7 ułatwia obsługę połączeń dynamicznych i parametry połączenia dzięki ulepszeniom systemu IDbConnectionInterceptor. Zaczyna się to od możliwości skonfigurowania DbContext
bez żadnych parametry połączenia. Na przykład:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer());
IDbConnectionInterceptor
Jedną z metod można następnie zaimplementować w celu skonfigurowania połączenia przed jego użyciem. ConnectionOpeningAsync
jest dobrym wyborem, ponieważ może wykonać operację asynchroniową w celu uzyskania parametry połączenia, znalezienia tokenu dostępu itd. Załóżmy na przykład, że usługa jest ograniczona do bieżącego żądania, które rozumie bieżącą dzierżawę:
services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();
Ostrzeżenie
Wykonywanie asynchronicznego wyszukiwania dla parametry połączenia, tokenu dostępu lub podobnego za każdym razem, gdy jest to konieczne, może być bardzo powolne. Rozważ buforowanie tych elementów i okresowe odświeżanie buforowanego ciągu lub tokenu. Na przykład tokeny dostępu mogą być często używane przez dłuższy czas przed koniecznością odświeżenia.
Można to wstrzyknąć do każdego DbContext
wystąpienia przy użyciu iniekcji konstruktora:
public class CustomerContext : DbContext
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public CustomerContext(
DbContextOptions<CustomerContext> options,
ITenantConnectionStringFactory connectionStringFactory)
: base(options)
{
_connectionStringFactory = connectionStringFactory;
}
// ...
}
Ta usługa jest następnie używana podczas konstruowania implementacji przechwytywania dla kontekstu:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
Na koniec przechwytywanie używa tej usługi do uzyskiwania parametry połączenia asynchronicznie i ustawia go po raz pierwszy, gdy połączenie jest używane:
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly IClientConnectionStringFactory _connectionStringFactory;
public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}
public override InterceptionResult ConnectionOpening(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result)
=> throw new NotSupportedException("Synchronous connections not supported.");
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = new())
{
if (string.IsNullOrEmpty(connection.ConnectionString))
{
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
}
return result;
}
}
Uwaga
Parametry połączenia jest uzyskiwany tylko przy pierwszym użyciu połączenia. Następnie parametry połączenia przechowywane w DbConnection
obiekcie będą używane bez wyszukiwania nowego parametry połączenia.
Napiwek
Ten przechwytywanie zastępuje metodę inną niż asynchronikowąConnectionOpening
, ponieważ usługa w celu pobrania parametry połączenia musi być wywoływana ze ścieżki kodu asynchronicznego.
Napiwek
Pokazany tutaj kod pochodzi z QueryStatisticsLoggerSample.cs.
Na koniec utwórzmy dwa przechwytniki, które współpracują ze sobą w celu wysyłania statystyk zapytań programu SQL Server do dziennika aplikacji. Aby wygenerować statystyki, musimy IDbCommandInterceptor wykonać dwie czynności.
Najpierw przechwytuje polecenia prefiksu za pomocą SET STATISTICS IO ON
polecenia , które informują program SQL Server o wysyłaniu statystyk do klienta po zużytym zestawie wyników:
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;
return new(result);
}
Po drugie, przechwytator zaimplementuje nową DataReaderClosingAsync
metodę, która jest wywoływana po zakończeniu DbDataReader korzystania z wyników, ale zanim zostanie zamknięta. Gdy program SQL Server wysyła statystyki, umieszcza je w drugim wyniku na czytniku, więc w tym momencie przechwytujący odczytuje ten wynik przez wywołanie NextResultAsync
, które wypełnia statystyki na połączeniu.
public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
DbCommand command,
DataReaderClosingEventData eventData,
InterceptionResult result)
{
await eventData.DataReader.NextResultAsync();
return result;
}
Drugi przechwytujący jest potrzebny do uzyskania statystyk z połączenia i zapisania ich w rejestratorze aplikacji. W tym celu użyjemy IDbConnectionInterceptormetody , implementowania nowej ConnectionCreated
metody. ConnectionCreated
jest wywoływana natychmiast po utworzeniu połączenia przez program EF Core, a więc może służyć do wykonywania dodatkowej konfiguracji tego połączenia. W takim przypadku przechwytujący uzyskuje element ILogger
, a następnie przechwytuje zdarzenie w SqlConnection.InfoMessage celu zarejestrowania komunikatów.
public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
{
logger.LogInformation(1, args.Message);
};
return result;
}
Ważne
Metody ConnectionCreating
i ConnectionCreated
są wywoływane tylko wtedy, gdy program EF Core tworzy DbConnection
obiekt . Nie będą one wywoływane, jeśli aplikacja utworzy obiekt DbConnection
i przekaże ją do platformy EF Core.
Uruchomienie kodu korzystającego z tych przechwytywania pokazuje statystyki zapytań programu SQL Server w dzienniku:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET STATISTICS IO ON;
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [Customers] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [PhoneNumber], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [PhoneNumber])
VALUES (i.[Name], i.[PhoneNumber])
OUTPUT INSERTED.[Id], i._Position;
info: InfoMessageLogger[1]
Table 'Customers'. Scan count 0, logical reads 5, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SET STATISTICS IO ON;
SELECT TOP(2) [c].[Id], [c].[Name], [c].[PhoneNumber]
FROM [Customers] AS [c]
WHERE [c].[Name] = N'Alice'
info: InfoMessageLogger[1]
Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
PROGRAM EF7 zawiera wiele ulepszeń tłumaczenia zapytań LINQ.
Napiwek
Pokazany tutaj kod pochodzi z GroupByFinalOperatorSample.cs.
Program EF7 obsługuje używanie GroupBy
jako operatora końcowego w zapytaniu. Na przykład to zapytanie LINQ:
var query = context.Books.GroupBy(s => s.Price);
Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]
Uwaga
Ten typ GroupBy
nie przekłada się bezpośrednio na język SQL, dlatego program EF Core wykonuje grupowanie w zwróconych wynikach. Nie powoduje to jednak transferu żadnych dodatkowych danych z serwera.
Napiwek
Pokazany tutaj kod pochodzi z GroupJoinFinalOperatorSample.cs.
Program EF7 obsługuje używanie GroupJoin
jako operatora końcowego w zapytaniu. Na przykład to zapytanie LINQ:
var query = context.Customers.GroupJoin(
context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });
Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId]
FROM [Customers] AS [c]
OUTER APPLY (
SELECT [o].[Id], [o].[Amount], [o].[CustomerId]
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId]
) AS [t]
ORDER BY [c].[Id]
Napiwek
Pokazany tutaj kod pochodzi z GroupByEntityTypeSample.cs.
Program EF7 obsługuje grupowanie według typu jednostki. Na przykład to zapytanie LINQ:
var query = context.Books
.GroupBy(s => s.Author)
.Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });
Tłumaczy się na następujący kod SQL podczas korzystania z biblioteki SQLite:
SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice]
FROM [Books] AS [b]
INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]
Należy pamiętać, że grupowanie według unikatowej właściwości, takiej jak klucz podstawowy, zawsze będzie bardziej wydajne niż grupowanie według typu jednostki. Jednak grupowanie według typów jednostek może być używane zarówno dla typów jednostek kluczy, jak i bez klucza.
Ponadto grupowanie według typu jednostki z kluczem podstawowym zawsze spowoduje utworzenie jednej grupy na wystąpienie jednostki, ponieważ każda jednostka musi mieć unikatową wartość klucza. Czasami warto przełączyć źródło zapytania, aby nie było wymagane grupowanie. Na przykład następujące zapytanie zwraca te same wyniki co poprzednie zapytanie:
var query = context.Authors
.Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });
To zapytanie przekłada się na następujący kod SQL podczas korzystania z biblioteki SQLite:
SELECT [a].[Id], [a].[Name], (
SELECT MAX([b].[Price])
FROM [Books] AS [b]
WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]
Napiwek
Pokazany tutaj kod pochodzi z UngroupedColumnsQuerySample.cs.
W programie EF Core 6.0 klauzula GROUP BY
odwołuje się do kolumn w zapytaniu zewnętrznym, która kończy się niepowodzeniem z niektórymi bazami danych i jest nieefektywna w innych. Rozważmy na przykład następujące zapytanie:
var query = from s in (from i in context.Invoices
group i by i.History.Month
into g
select new { Month = g.Key, Total = g.Sum(p => p.Amount), })
select new
{
s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount)
};
W programie EF Core 6.0 w programie SQL Server zostało to przetłumaczone na:
SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], (
SELECT COALESCE(SUM([p].[Amount]), 0.0)
FROM [Payments] AS [p]
WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment]
FROM [Invoices] AS [i]
GROUP BY DATEPART(month, [i].[History])
W programie EF7 tłumaczenie brzmi:
SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], (
SELECT COALESCE(SUM([p].[Amount]), 0.0)
FROM [Payments] AS [p]
WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment]
FROM (
SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key]
FROM [Invoices] AS [i]
) AS [t]
GROUP BY [t].[Key]
Napiwek
Pokazany tutaj kod pochodzi z ReadOnlySetQuerySample.cs.
Program EF7 obsługuje funkcję , Contains
gdy elementy do wyszukania znajdują się w elemecie IReadOnlySet
lub IReadOnlyCollection
lub IReadOnlyList
. Na przykład to zapytanie LINQ:
IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));
Przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5))
Program EF7 wprowadza lepszą rozszerzalność dla dostawców w celu tłumaczenia funkcji agregujących. To i inne prace w tym obszarze doprowadziły do kilku nowych tłumaczeń między dostawcami, w tym:
String.Join
String.Concat
Uwaga
Funkcje agregujące, które działają na IEnumerable
argument, są zwykle tłumaczone tylko w GroupBy
zapytaniach. Zagłosuj na obsługę typów przestrzennych w kolumnach JSON, jeśli chcesz usunąć to ograniczenie.
Napiwek
Pokazany tutaj kod pochodzi z StringAggregateFunctionsSample.cs.
Zapytania korzystające z funkcji Join i Concat są teraz tłumaczone, gdy jest to konieczne. Na przykład:
var query = context.Posts
.GroupBy(post => post.Author)
.Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });
To zapytanie przekłada się na następujące kwestie podczas korzystania z programu SQL Server:
SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]
W połączeniu z innymi funkcjami ciągów te tłumaczenia umożliwiają pewne złożone manipulowanie ciągami na serwerze. Na przykład:
var query = context.Posts
.GroupBy(post => post.Author!.Name)
.Select(
grouping =>
new
{
PostAuthor = grouping.Key,
Blogs = string.Concat(
grouping
.Select(post => post.Blog.Name)
.Distinct()
.Select(postName => "'" + postName + "' ")),
ContentSummaries = string.Join(
" | ",
grouping
.Where(post => post.Content.Length >= 10)
.Select(post => "'" + post.Content.Substring(0, 10) + "' "))
});
To zapytanie przekłada się na następujące kwestie podczas korzystania z programu SQL Server:
SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c]
FROM (
SELECT [a].[Name], COALESCE(STRING_AGG(CASE
WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'')
END, N' | '), N'') AS [c]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Name]
) AS [t]
OUTER APPLY (
SELECT DISTINCT [b].[Name]
FROM [Posts] AS [p0]
LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id]
INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id]
WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL)
) AS [t0]
ORDER BY [t].[Name]
Napiwek
Pokazany tutaj kod pochodzi z SpatialAggregateFunctionsSample.cs.
Obecnie jest możliwe, aby dostawcy baz danych, którzy obsługują aplikację NetTopologySuite , aby przetłumaczyć następujące funkcje agregacji przestrzennej:
Napiwek
Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server i sqLite. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.
Na przykład:
var query = context.Caches
.Where(cache => cache.Location.X < -90)
.GroupBy(cache => cache.Owner)
.Select(
grouping => new { Id = grouping.Key, Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) });
To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined]
FROM [Caches] AS [c]
WHERE [c].[Location].Long < -90.0E0
GROUP BY [c].[Owner]
Napiwek
Pokazany tutaj kod pochodzi z StatisticalAggregateFunctionsSample.cs.
Tłumaczenia programu SQL Server zostały zaimplementowane dla następujących funkcji statystycznych:
Napiwek
Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.
Na przykład:
var query = context.Downloads
.GroupBy(download => download.Uploader.Id)
.Select(
grouping => new
{
Author = grouping.Key,
TotalCost = grouping.Sum(d => d.DownloadCount),
AverageViews = grouping.Average(d => d.DownloadCount),
VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)),
VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)),
StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)),
StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount))
});
To zapytanie jest tłumaczone na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample]
FROM [Downloads] AS [d]
INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id]
GROUP BY [u].[Id]
Napiwek
Pokazany tutaj kod pochodzi z MiscellaneousTranslationsSample.cs.
Program EF7 tłumaczy String.IndexOf teraz w zapytaniach LINQ. Na przykład:
var query = context.Posts
.Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
.Where(post => post.IndexOfEntity > 0);
To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity]
FROM [Posts] AS [p]
WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0
Napiwek
Pokazany tutaj kod pochodzi z MiscellaneousTranslationsSample.cs.
Program EF7 tłumaczy Object.GetType() teraz w zapytaniach LINQ. Na przykład:
var query = context.Posts.Where(post => post.GetType() == typeof(Post));
To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server z dziedziczeniem TPH:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'
Zwróć uwagę, że to zapytanie zwraca tylko Post
wystąpienia faktycznie typu Post
, a nie te z żadnych typów pochodnych. Różni się to od zapytania, które używa is
metody lub OfType
, która zwróci również wystąpienia dowolnych typów pochodnych. Rozważmy na przykład zapytanie:
var query = context.Posts.OfType<Post>();
Co przekłada się na inny język SQL:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
I zwróci zarówno jednostki, jak Post
i FeaturedPost
.
Napiwek
Pokazany tutaj kod pochodzi z MiscellaneousTranslationsSample.cs.
Program EF7 wprowadza nowe AtTimeZone funkcje dla i DateTimeOffsetDateTime . Te funkcje tłumaczą się na AT TIME ZONE
klauzule w wygenerowanych bazach SQL. Na przykład:
var query = context.Posts
.Select(
post => new
{
post.Title,
PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"),
UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"),
});
To zapytanie przekłada się na następujący kod SQL podczas korzystania z programu SQL Server:
SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime]
FROM [Posts] AS [p]
Napiwek
Te tłumaczenia zostały zaimplementowane przez zespół dla programu SQL Server. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby dodać pomoc techniczną, jeśli został on wdrożony dla tego dostawcy.
Napiwek
Pokazany tutaj kod pochodzi z MiscellaneousTranslationsSample.cs.
Metody Include można teraz używać z EF.Property. Umożliwia to filtrowanie i porządkowanie nawet dla prywatnych właściwości nawigacji lub prywatnych nawigacji reprezentowanych przez pola. Na przykład:
var query = context.Blogs.Include(
blog => EF.Property<ICollection<Post>>(blog, "Posts")
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Jest to odpowiednik:
var query = context.Blogs.Include(
blog => Posts
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Ale nie musi Blog.Posts
być publicznie dostępny.
W przypadku korzystania z programu SQL Server oba powyższe zapytania tłumaczą się na:
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText]
FROM [Blogs] AS [b]
LEFT JOIN (
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Content] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Title]
Napiwek
Pokazany tutaj kod pochodzi z CosmosQueriesSample.cs.
Program EF7 obsługuje używanie Regex.IsMatch zapytań LINQ w usłudze Azure Cosmos DB. Na przykład:
var containsInnerT = await context.Triangles
.Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
.ToListAsync();
Przekłada się na następujący kod SQL:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))
PROGRAM EF7 zawiera różne małe ulepszenia DbContext klas i powiązane.
Napiwek
Kod przykładów w tej sekcji pochodzi z DbContextApiSample.cs.
Publiczne, ustawiane DbSet
właściwości na obiekcie DbContext
są automatycznie inicjowane przez program EF Core podczas DbContext
konstruowania. Rozważmy na przykład następującą DbContext
definicję:
public class SomeDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}
Właściwość Blogs
zostanie ustawiona na DbSet<Blog>
wystąpienie w ramach konstruowania DbContext
wystąpienia. Dzięki temu kontekst może być używany dla zapytań bez żadnych dodatkowych kroków.
Jednak po wprowadzeniu typów odwołań dopuszczających wartość null w języku C# kompilator ostrzega teraz, że właściwość Blogs
niezwiązana z wartością null nie została zainicjowana:
[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Jest to fałszywe ostrzeżenie; właściwość jest ustawiona na wartość inną niż null przez program EF Core. Ponadto deklarowanie właściwości jako dopuszczającej wartość null spowoduje, że ostrzeżenie zniknie, ale nie jest to dobry pomysł, ponieważ, koncepcyjnie, właściwość nie jest dopuszczana do wartości null i nigdy nie będzie mieć wartości null.
Program EF7 zawiera element DiagnosticSuppressor dla DbSet
właściwości, DbContext
które uniemożliwiają kompilatorowi wygenerowanie tego ostrzeżenia.
Napiwek
Ten wzorzec pochodzi z dni, w których właściwości automatyczne języka C# były bardzo ograniczone. W przypadku nowoczesnego języka C# rozważ utworzenie właściwości automatycznych tylko do odczytu, a następnie jawne zainicjowanie ich w konstruktorze DbContext
lub uzyskanie buforowanego DbSet
wystąpienia z kontekstu w razie potrzeby. Na przykład public DbSet<Blog> Blogs => Set<Blog>()
.
Czasami aplikacja jawnie anuluje zapytanie lub inną operację bazy danych. Zwykle odbywa się to przy użyciu metody przekazanej CancellationToken do metody wykonującej operację.
W programie EF Core 6 zdarzenia rejestrowane po anulowaniu operacji są takie same jak te rejestrowane, gdy operacja kończy się niepowodzeniem z jakiegoś innego powodu. Program EF7 wprowadza nowe zdarzenia dziennika przeznaczone specjalnie dla anulowanych operacji bazy danych. Te nowe zdarzenia są domyślnie rejestrowane na Debug poziomie. W poniższej tabeli przedstawiono odpowiednie zdarzenia i ich domyślne poziomy dziennika:
Wydarzenie | opis | Domyślny poziom dziennika |
---|---|---|
CoreEventId.QueryIterationFailed | Wystąpił błąd podczas przetwarzania wyników zapytania. | LogLevel.Error |
CoreEventId.SaveChangesFailed | Wystąpił błąd podczas próby zapisania zmian w bazie danych. | LogLevel.Error |
RelationalEventId.CommandError | Wystąpił błąd podczas wykonywania polecenia bazy danych. | LogLevel.Error |
CoreEventId.QueryCanceled | Zapytanie zostało anulowane. | LogLevel.Debug |
CoreEventId.SaveChangesCanceled | Polecenie bazy danych zostało anulowane podczas próby zapisania zmian. | LogLevel.Debug |
RelationalEventId.CommandCanceled | Wykonanie obiektu DbCommand zostało anulowane. |
LogLevel.Debug |
Uwaga
Anulowanie jest wykrywane przez sprawdzenie wyjątku zamiast sprawdzania tokenu anulowania. Oznacza to, że anulowania nie są wyzwalane za pośrednictwem tokenu anulowania nadal będą wykrywane i rejestrowane w ten sposób.
Kod pracujący z modelem EF często ma IProperty INavigation metadane właściwości lub nawigacji lub reprezentujące je. JednostkaEntry jest następnie używana do pobierania wartości właściwości/nawigacji lub wykonywania zapytania o jego stan. Jednak przed ef7, to wymaga przekazania nazwy właściwości lub nawigacji do metod EntityEntry
, które następnie ponownie wyszukać IProperty
lub INavigation
. W programie EF7 IProperty
element lub INavigation
można przekazać bezpośrednio, unikając dodatkowego wyszukiwania.
Rozważmy na przykład metodę znalezienia wszystkich elementów równorzędnych danej jednostki:
public static IEnumerable<TEntity> FindSiblings<TEntity>(
this DbContext context, TEntity entity, string navigationToParent)
where TEntity : class
{
var parentEntry = context.Entry(entity).Reference(navigationToParent);
return context.Entry(parentEntry.CurrentValue!)
.Collection(parentEntry.Metadata.Inverse!)
.CurrentValue!
.OfType<TEntity>()
.Where(e => !ReferenceEquals(e, entity));
}
Ta metoda znajduje element nadrzędny danej jednostki, a następnie przekazuje odwrotność INavigation
do Collection
metody wpisu nadrzędnego. Te metadane są następnie używane do zwracania wszystkich elementów równorzędnych danego elementu nadrzędnego. Oto przykład użycia:
Console.WriteLine($"Siblings to {post.Id}: '{post.Title}' are...");
foreach (var sibling in context.FindSiblings(post, nameof(post.Blog)))
{
Console.WriteLine($" {sibling.Id}: '{sibling.Title}'");
}
Dane wyjściowe:
Siblings to 1: 'Announcing Entity Framework 7 Preview 7: Interceptors!' are...
5: 'Productivity comes to .NET MAUI in Visual Studio 2022'
6: 'Announcing .NET 7 Preview 7'
7: 'ASP.NET Core updates in .NET 7 Preview 7'
Program EF Core może używać tego samego typu CLR dla wielu różnych typów jednostek. Są one nazywane typami jednostek typu współużytkowanego i są często używane do mapowania typu słownika z parami klucz/wartość używanymi dla właściwości typu jednostki. Na przykład BuildMetadata
typ jednostki można zdefiniować bez definiowania dedykowanego typu CLR:
modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
"BuildMetadata", b =>
{
b.IndexerProperty<int>("Id");
b.IndexerProperty<string>("Tag");
b.IndexerProperty<Version>("Version");
b.IndexerProperty<string>("Hash");
b.IndexerProperty<bool>("Prerelease");
});
Zwróć uwagę, że typ jednostki typu współużytkowanego musi mieć nazwę — w tym przypadku nazwa to BuildMetadata
. Te typy jednostek są następnie używane przy użyciu DbSet
typu jednostki, który jest uzyskiwany przy użyciu nazwy. Na przykład:
public DbSet<Dictionary<string, object>> BuildMetadata
=> Set<Dictionary<string, object>>("BuildMetadata");
Może to DbSet
służyć do śledzenia wystąpień jednostek:
await context.BuildMetadata.AddAsync(
new Dictionary<string, object>
{
{ "Tag", "v7.0.0-rc.1.22426.7" },
{ "Version", new Version(7, 0, 0) },
{ "Prerelease", true },
{ "Hash", "dc0f3e8ef10eb1464b27f0fd4704f53c01226036" }
});
I wykonaj zapytania:
var builds = await context.BuildMetadata
.Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
.OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
.ToListAsync();
Teraz w programie EF7 istnieje również Entry
metoda, na DbSet
której można użyć do uzyskania stanu wystąpienia, nawet jeśli nie jest jeszcze śledzona. Na przykład:
var state = context.BuildMetadata.Entry(build).State;
W programie EF7 zdarzenie ContextInitialized jest rejestrowane na Debug poziomie. Na przykład:
dbug: 10/7/2022 12:27:52.379 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite
W poprzednich wersjach była rejestrowana na Information poziomie. Na przykład:
info: 10/7/2022 12:30:34.757 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite
W razie potrzeby poziom dziennika można zmienić z powrotem na Information
:
optionsBuilder.ConfigureWarnings(
builder =>
{
builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
});
W programie EF7 IEntityEntryGraphIterator usługa może być używana przez aplikacje. Jest to usługa używana wewnętrznie podczas odnajdywania grafu jednostek do śledzenia, a także przez TrackGraphelement . Oto przykład, który iteruje wszystkie jednostki osiągalne od pewnej jednostki początkowej:
var blogEntry = context.ChangeTracker.Entries<Blog>().First();
var found = new HashSet<object>();
var iterator = context.GetService<IEntityEntryGraphIterator>();
iterator.TraverseGraph(new EntityEntryGraphNode<HashSet<object>>(blogEntry, found, null, null), node =>
{
if (node.NodeState.Contains(node.Entry.Entity))
{
return false;
}
Console.Write($"Found with '{node.Entry.Entity.GetType().Name}'");
if (node.InboundNavigation != null)
{
Console.Write($" by traversing '{node.InboundNavigation.Name}' from '{node.SourceEntry!.Entity.GetType().Name}'");
}
Console.WriteLine();
node.NodeState.Add(node.Entry.Entity);
return true;
});
Console.WriteLine();
Console.WriteLine($"Finished iterating. Found {found.Count} entities.");
Console.WriteLine();
Uwaga:
false
. Ten przykład śledzi odwiedzone jednostki i zwraca informacje false
o tym, kiedy jednostka została już odwiedzona. Zapobiega to nieskończonym pętlom wynikającym z cykli na wykresie.EntityEntryGraphNode<TState>
umożliwia przekazanie stanu bez przechwytywania go do delegata.PROGRAM EF7 zawiera różne małe ulepszenia w tworzeniu modeli.
Napiwek
Kod przykładów w tej sekcji pochodzi z ModelBuildingSample.cs.
Domyślnie program EF Core tworzy indeksy rosnące. Program EF7 obsługuje również tworzenie indeksów malejących. Na przykład:
modelBuilder
.Entity<Post>()
.HasIndex(post => post.Title)
.IsDescending();
Możesz też użyć atrybutu Index
mapowania:
[Index(nameof(Title), AllDescending = true)]
public class Post
{
public int Id { get; set; }
[MaxLength(64)]
public string? Title { get; set; }
}
Jest to rzadko przydatne w przypadku indeksów w pojedynczej kolumnie, ponieważ baza danych może używać tego samego indeksu do porządkowania w obu kierunkach. Jednak nie jest tak w przypadku indeksów złożonych w wielu kolumnach, w których kolejność w każdej kolumnie może być ważna. Program EF Core obsługuje tę funkcję, umożliwiając wielu kolumnom definiowanie innej kolejności dla każdej kolumny. Na przykład:
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner })
.IsDescending(false, true);
Możesz też użyć atrybutu mapowania:
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })]
public class Blog
{
public int Id { get; set; }
[MaxLength(64)]
public string? Name { get; set; }
[MaxLength(64)]
public string? Owner { get; set; }
public List<Post> Posts { get; } = new();
}
Spowoduje to wykonanie następujących czynności sql podczas korzystania z programu SQL Server:
CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);
Na koniec można utworzyć wiele indeksów w tym samym uporządkowanym zestawie kolumn, podając nazwy indeksów. Na przykład:
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_1")
.IsDescending(false, true);
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_2")
.IsDescending(true, true);
Możesz też użyć atrybutów mapowania:
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true }, Name = "IX_Blogs_Name_Owner_1")]
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { true, true }, Name = "IX_Blogs_Name_Owner_2")]
public class Blog
{
public int Id { get; set; }
[MaxLength(64)]
public string? Name { get; set; }
[MaxLength(64)]
public string? Owner { get; set; }
public List<Post> Posts { get; } = new();
}
Spowoduje to wygenerowanie następującego kodu SQL w programie SQL Server:
CREATE INDEX [IX_Blogs_Name_Owner_1] ON [Blogs] ([Name], [Owner] DESC);
CREATE INDEX [IX_Blogs_Name_Owner_2] ON [Blogs] ([Name] DESC, [Owner] DESC);
Program EF7 wprowadza nowy atrybut mapowania (np. "adnotacja danych") do określania właściwości klucza podstawowego lub właściwości dowolnego typu jednostki. W przeciwieństwie do System.ComponentModel.DataAnnotations.KeyAttributeklasy , PrimaryKeyAttribute jest umieszczany w klasie typu jednostki, a nie we właściwości klucza. Na przykład:
[PrimaryKey(nameof(PostKey))]
public class Post
{
public int PostKey { get; set; }
}
Dzięki temu jest to naturalne dopasowanie do definiowania kluczy złożonych:
[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
public int PostId { get; set; }
public int CommentId { get; set; }
public string CommentText { get; set; } = null!;
}
Zdefiniowanie indeksu w klasie oznacza również, że może służyć do określania właściwości prywatnych lub pól jako kluczy, mimo że zwykle byłyby one ignorowane podczas kompilowania modelu EF. Na przykład:
[PrimaryKey(nameof(_id))]
public class Tag
{
private readonly int _id;
}
Program EF7 wprowadza atrybut mapowania (np. "adnotację danych"), aby określić DeleteBehavior dla relacji. Na przykład wymagane relacje są tworzone DeleteBehavior.Cascade domyślnie. Można to zmienić domyślnie DeleteBehavior.NoAction przy użyciu polecenia DeleteBehaviorAttribute:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
[DeleteBehavior(DeleteBehavior.NoAction)]
public Blog Blog { get; set; } = null!;
}
Spowoduje to wyłączenie usuwania kaskadowego dla relacji Wpisy w blogu.
Niektóre wzorce mapowania powodują mapowanie tej samej właściwości CLR na kolumnę w każdej z wielu różnych tabel. Program EF7 umożliwia tym kolumnom różne nazwy. Rozważmy na przykład prostą hierarchię dziedziczenia:
public abstract class Animal
{
public int Id { get; set; }
public string Breed { get; set; } = null!;
}
public class Cat : Animal
{
public string? EducationalLevel { get; set; }
}
public class Dog : Animal
{
public string? FavoriteToy { get; set; }
}
Dzięki strategii mapowania dziedziczenia TPT te typy zostaną zamapowane na trzy tabele. Jednak kolumna klucza podstawowego w każdej tabeli może mieć inną nazwę. Na przykład:
CREATE TABLE [Animals] (
[Id] int NOT NULL IDENTITY,
[Breed] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);
CREATE TABLE [Cats] (
[CatId] int NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
Program EF7 umożliwia skonfigurowanie tego mapowania przy użyciu konstruktora zagnieżdżonych tabel:
modelBuilder.Entity<Animal>().ToTable("Animals");
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));
Za pomocą mapowania Breed
dziedziczenia TPC właściwość może być również mapowana na różne nazwy kolumn w różnych tabelach. Rozważmy na przykład następujące tabele TPC:
CREATE TABLE [Cats] (
[CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[CatBreed] nvarchar(max) NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[DogBreed] nvarchar(max) NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);
Program EF7 obsługuje to mapowanie tabeli:
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
builder =>
{
builder.Property(cat => cat.Id).HasColumnName("CatId");
builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
});
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
builder =>
{
builder.Property(dog => dog.Id).HasColumnName("DogId");
builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
});
Program EF7 obsługuje relacje wiele-do-wielu, w których jedna lub druga nie ma właściwości nawigacji. Rozważmy na przykład Post
typy i Tag
:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
public Blog Blog { get; set; } = null!;
public List<Tag> Tags { get; } = new();
}
public class Tag
{
public int Id { get; set; }
public string TagName { get; set; } = null!;
}
Zwróć uwagę, że Post
typ ma właściwość nawigacji dla listy tagów, ale Tag
typ nie ma właściwości nawigacji dla wpisów. W programie EF7 można to nadal skonfigurować jako relację wiele-do-wielu, umożliwiając użycie tego samego Tag
obiektu dla wielu różnych wpisów. Na przykład:
modelBuilder
.Entity<Post>()
.HasMany(post => post.Tags)
.WithMany();
Spowoduje to mapowanie na odpowiednią tabelę sprzężenia:
CREATE TABLE [Tags] (
[Id] int NOT NULL IDENTITY,
[TagName] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([Id])
);
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(64) NULL,
[BlogId] int NOT NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
);
CREATE TABLE [PostTag] (
[PostId] int NOT NULL,
[TagsId] int NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE
);
A relacja może być używana jako wiele-do-wielu w normalny sposób. Na przykład wstawianie niektórych wpisów, które współużytkuje różne tagi z wspólnego zestawu:
var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, };
await context.AddRangeAsync(new Blog { Posts =
{
new Post { Tags = { tags[0], tags[1] } },
new Post { Tags = { tags[1], tags[0], tags[2] } },
new Post()
} });
await context.SaveChangesAsync();
Dzielenie jednostek mapuje jeden typ jednostki na wiele tabel. Rozważmy na przykład bazę danych z trzema tabelami, które przechowują dane klienta:
Customers
informacji o klientachPhoneNumbers
numeru telefonu klientaAddresses
Tabela adresu klientaPoniżej przedstawiono definicje tych tabel w programie SQL Server:
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [PhoneNumbers] (
[CustomerId] int NOT NULL,
[PhoneNumber] nvarchar(max) NULL,
CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Addresses] (
[CustomerId] int NOT NULL,
[Street] nvarchar(max) NOT NULL,
[City] nvarchar(max) NOT NULL,
[PostCode] nvarchar(max) NULL,
[Country] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
Każda z tych tabel jest zwykle mapowana na własny typ jednostki z relacjami między typami. Jeśli jednak wszystkie trzy tabele są zawsze używane razem, bardziej wygodne może być mapowania ich wszystkich na pojedynczy typ jednostki. Na przykład:
public class Customer
{
public Customer(string name, string street, string city, string? postCode, string country)
{
Name = name;
Street = street;
City = city;
PostCode = postCode;
Country = country;
}
public int Id { get; set; }
public string Name { get; set; }
public string? PhoneNumber { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string? PostCode { get; set; }
public string Country { get; set; }
}
Jest to osiągane w programie EF7 przez wywołanie SplitToTable
każdego podziału w typie jednostki. Na przykład poniższy kod dzieli Customer
typ jednostki na Customers
tabele , PhoneNumbers
i Addresses
pokazane powyżej:
modelBuilder.Entity<Customer>(
entityBuilder =>
{
entityBuilder
.ToTable("Customers")
.SplitToTable(
"PhoneNumbers",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.PhoneNumber);
})
.SplitToTable(
"Addresses",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.Street);
tableBuilder.Property(customer => customer.City);
tableBuilder.Property(customer => customer.PostCode);
tableBuilder.Property(customer => customer.Country);
});
});
Należy również zauważyć, że w razie potrzeby można określić różne nazwy kolumn klucza podstawowego dla każdej z tabel.
Ciągi Unicode programu SQL Server reprezentowane przez nchar
typy danych i nvarchar
są przechowywane jako UTF-16. Ponadto char
typy danych i varchar
służą do przechowywania ciągów innych niż Unicode z obsługą różnych zestawów znaków.
Począwszy od programu SQL Server 2019, char
typy danych i varchar
mogą zamiast tego przechowywać ciągi Unicode z kodowaniem UTF-8 . Element jest osiągany przez ustawienie jednego z sortowania UTF-8. Na przykład poniższy kod konfiguruje zmienną długość ciągu UTF-8 programu SQL Server dla kolumny CommentText
:
modelBuilder
.Entity<Comment>()
.Property(comment => comment.CommentText)
.HasColumnType("varchar(max)")
.UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");
Ta konfiguracja generuje następującą definicję kolumny programu SQL Server:
CREATE TABLE [Comment] (
[PostId] int NOT NULL,
[CommentId] int NOT NULL,
[CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL,
CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId])
);
Mapowanie tabel czasowych programu EF Core SQL Server zostało ulepszone w programie EF7 w celu obsługi udostępniania tabel. W szczególności domyślne mapowanie dla należących do nich pojedynczych jednostek używa udostępniania tabel.
Rozważmy na przykład typ Employee
jednostki właściciela i jej typ jednostki EmployeeInfo
należącej do niego:
public class Employee
{
public Guid EmployeeId { get; set; }
public string Name { get; set; } = null!;
public EmployeeInfo Info { get; set; } = null!;
}
public class EmployeeInfo
{
public string Position { get; set; } = null!;
public string Department { get; set; } = null!;
public string? Address { get; set; }
public decimal? AnnualSalary { get; set; }
}
Jeśli te typy są mapowane na tę samą tabelę, w programie EF7 ta tabela może być tabelą czasową:
modelBuilder
.Entity<Employee>()
.ToTable(
"Employees",
tableBuilder =>
{
tableBuilder.IsTemporal();
tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
})
.OwnsOne(
employee => employee.Info,
ownedBuilder => ownedBuilder.ToTable(
"Employees",
tableBuilder =>
{
tableBuilder.IsTemporal();
tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
}));
Uwaga
Ułatwianie tej konfiguracji jest śledzone przez problem nr 29303. Zagłosuj na ten problem, jeśli jest to coś, co chcesz zobaczyć zaimplementowane.
Program EF7 zawiera dwie znaczące ulepszenia automatycznego generowania wartości dla kluczowych właściwości.
Napiwek
Kod przykładów w tej sekcji pochodzi z ValueGenerationSample.cs.
W projekcie opartym na domenie (DDD) "klucze chronione" mogą poprawić bezpieczeństwo typu kluczowych właściwości. Jest to osiągane przez zawijanie typu klucza w innym typie, który jest specyficzny dla użycia klucza. Na przykład poniższy kod definiuje ProductId
typ kluczy produktów i CategoryId
typ kluczy kategorii.
public readonly struct ProductId
{
public ProductId(int value) => Value = value;
public int Value { get; }
}
public readonly struct CategoryId
{
public CategoryId(int value) => Value = value;
public int Value { get; }
}
Są one następnie używane w Product
typach jednostek i Category
:
public class Product
{
public Product(string name) => Name = name;
public ProductId Id { get; set; }
public string Name { get; set; }
public CategoryId CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
public class Category
{
public Category(string name) => Name = name;
public CategoryId Id { get; set; }
public string Name { get; set; }
public List<Product> Products { get; } = new();
}
Uniemożliwia to przypadkowe przypisanie identyfikatora kategorii do produktu lub odwrotnie.
Ostrzeżenie
Podobnie jak w przypadku wielu koncepcji DDD, to ulepszone bezpieczeństwo typów wiąże się z kosztem dodatkowej złożoności kodu. Warto rozważyć, czy na przykład przypisanie identyfikatora produktu do kategorii jest czymś, co kiedykolwiek może się zdarzyć. Utrzymywanie prostych rzeczy może być ogólnie bardziej korzystne dla bazy kodu.
Chronione typy kluczy pokazane tutaj oba zawijają int
wartości kluczy, co oznacza, że wartości całkowite będą używane w zamapowanych tabelach bazy danych. Jest to osiągane przez zdefiniowanie konwerterów wartości dla typów:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}
private class ProductIdConverter : ValueConverter<ProductId, int>
{
public ProductIdConverter()
: base(v => v.Value, v => new(v))
{
}
}
private class CategoryIdConverter : ValueConverter<CategoryId, int>
{
public CategoryIdConverter()
: base(v => v.Value, v => new(v))
{
}
}
Uwaga
W tym miejscu kod używa struct
typów. Oznacza to, że mają odpowiednie semantyki typu wartości do użycia jako klucze. Jeśli class
zamiast tego są używane typy, należy zastąpić semantyka równości lub określić porównanie wartości.
W programie EF7 typy kluczy oparte na konwerterach wartości mogą używać automatycznie generowanych wartości kluczy, o ile podstawowy typ obsługuje ten typ. Jest to skonfigurowane w normalny sposób przy użyciu polecenia ValueGeneratedOnAdd
:
modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();
Domyślnie powoduje to użycie kolumn w IDENTITY
przypadku użycia z programem SQL Server:
CREATE TABLE [Categories] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY ([Id]));
CREATE TABLE [Products] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[CategoryId] int NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);
Które są używane w normalny sposób do generowania wartości kluczy podczas wstawiania jednostek:
MERGE [Categories] USING (
VALUES (@p0, 0),
(@p1, 1)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;
Program EF Core obsługuje generowanie wartości klucza przy użyciu kolumn programu SQL Server IDENTITY
lub wzorzec Hi-Lo oparty na blokach kluczy generowanych przez sekwencję bazy danych. Program EF7 wprowadza obsługę sekwencji bazy danych dołączonej do domyślnego ograniczenia kolumny klucza. W najprostszej formie wymaga to jedynie poinformowania platformy EF Core o użyciu sekwencji dla właściwości klucza:
modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();
Spowoduje to zdefiniowanie sekwencji w bazie danych:
CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;
Który jest następnie używany w domyślnym ograniczeniu kolumny klucza:
CREATE TABLE [Products] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]),
[Name] nvarchar(max) NOT NULL,
[CategoryId] int NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);
Uwaga
Ta forma generowania kluczy jest używana domyślnie do generowania kluczy w hierarchiach typów jednostek przy użyciu strategii mapowania TPC.
W razie potrzeby sekwencja może mieć inną nazwę i schemat. Na przykład:
modelBuilder
.Entity<Product>()
.Property(product => product.Id)
.UseSequence("ProductsSequence", "northwind");
Dalsza konfiguracja sekwencji jest tworzona przez jawne skonfigurowanie jej w modelu. Na przykład:
modelBuilder
.HasSequence<int>("ProductsSequence", "northwind")
.StartsAt(1000)
.IncrementsBy(2);
Program EF7 zawiera dwie znaczące ulepszenia w przypadku korzystania z narzędzi wiersza polecenia migracji platformy EF Core.
Bardzo często odczytuje parametry połączenia z pliku konfiguracji, a następnie przekazuje ten parametry połączenia do UseSqlServer
metody , UseSqlite
lub równoważnej metody dla innego dostawcy. Na przykład:
services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));
Podczas stosowania migracji często zdarza się również przekazać parametry połączenia. Na przykład:
dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
Lub w przypadku korzystania z pakietu Migracje.
./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
W takim przypadku, mimo że parametry połączenia odczyt z konfiguracji nie jest używany, kod uruchamiania aplikacji nadal próbuje odczytać go z konfiguracji i przekazać go do UseSqlServer
. Jeśli konfiguracja jest niedostępna, spowoduje to przekazanie wartości null do UseSqlServer
. W programie EF7 jest to dozwolone, o ile parametry połączenia zostanie ostatecznie ustawiona później, na przykład przez przekazanie --connection
do narzędzia wiersza polecenia.
Uwaga
Ta zmiana została wprowadzona dla UseSqlServer
i UseSqlite
. W przypadku innych dostawców skontaktuj się z opiekunem dostawcy, aby wprowadzić równoważną zmianę, jeśli nie została jeszcze wykonana dla tego dostawcy.
Program EF Core uruchamia kod aplikacji, gdy dotnet-ef
są używane polecenia programu PowerShell lub . Czasami może być konieczne wykrycie tej sytuacji, aby zapobiec wykonywaniu nieodpowiedniego kodu w czasie projektowania. Na przykład kod, który automatycznie stosuje migracje podczas uruchamiania, prawdopodobnie nie powinien tego robić w czasie projektowania. W programie EF7 można to wykryć przy użyciu flagi EF.IsDesignTime
:
if (!EF.IsDesignTime)
{
await context.Database.MigrateAsync();
}
Program EF Core ustawia wartość IsDesignTime
na true
wartość , gdy kod aplikacji jest uruchamiany w imieniu narzędzi.
Program EF Core obsługuje dynamicznie generowane serwery proxy na potrzeby ładowania leniwego i śledzenia zmian. Program EF7 zawiera dwie ulepszenia wydajności podczas korzystania z tych serwerów proxy:
Poniżej przedstawiono kilka wyników wydajności modelu z 449 typami jednostek, właściwościami 6390 i relacjami 720.
Scenariusz | Method | Średnia | Błąd | StdDev |
---|---|---|---|---|
Program EF Core 6.0 bez serwerów proxy | TimeToFirstQuery | 1,085 s | 0,0083 s | 0,0167 s |
Program EF Core 6.0 z serwerami proxy śledzenia zmian | TimeToFirstQuery | 13.01 s | 0,2040 s | 0,4110 s |
Program EF Core 7.0 bez serwerów proxy | TimeToFirstQuery | 1.442 s | 0,0134 s | 0,0272 s |
Program EF Core 7.0 z serwerami proxy śledzenia zmian | TimeToFirstQuery | 1,446 s | 0,0160 s | 0,0323 s |
Program EF Core 7.0 z serwerami proxy śledzenia zmian i skompilowanym modelem | TimeToFirstQuery | 0,162 s | 0,0062 s | 0,0125 s |
W takim przypadku model z serwerami proxy śledzenia zmian może być gotowy do wykonania pierwszego zapytania 80 razy szybciej w programie EF7, niż było to możliwe w przypadku programu EF Core 6.0.
Zespół windows Forms wprowadza pewne wspaniałe ulepszenia środowiska projektanta programu Visual Studio. Obejmuje to nowe środowiska powiązań danych, które dobrze integrują się z platformą EF Core.
Krótko mówiąc, nowe środowisko udostępnia program Visual Studio U.I. do tworzenia elementu ObjectDataSource:
Można to następnie powiązać z platformą EF Core DbSet
za pomocą prostego kodu:
public partial class MainForm : Form
{
private ProductsContext? dbContext;
public MainForm()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.dbContext = new ProductsContext();
this.dbContext.Categories.Load();
this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
this.dbContext?.Dispose();
this.dbContext = null;
}
}
Zobacz Wprowadzenie do formularzy systemu Windows, aby zapoznać się z kompletnym przewodnikiem i pobrać przykładową aplikację WinForms.
Opinia o produkcie .NET
.NET to projekt typu open source. Wybierz link, aby przekazać opinię:
Szkolenie
Moduł
Modyfikowanie danych przy użyciu języka T-SQL - Training
Modyfikowanie danych przy użyciu języka T-SQL