Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
EF Core 7.0 (EF7) släpptes i november 2022.
Tips/Råd
Du kan köra och felsöka i exemplen genom att ladda ned exempelkoden från GitHub. Varje avsnitt länkar till källkoden som är specifik för det avsnittet.
EF7 riktar in sig på .NET 6 och kan därför användas med antingen .NET 6 (LTS) eller .NET 7.
Exempelmodell
Många av exemplen nedan använder en enkel modell med bloggar, inlägg, taggar och författare:
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();
}
Några av exemplen använder också aggregeringstyper, som mappas på olika sätt i olika exempel. Det finns en samlingstyp för kontakter:
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; }
}
Och en andra aggregeringstyp för postmetadata:
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; }
}
Tips/Råd
Exempelmodellen finns i BlogsContext.cs.
JSON-kolumner
De flesta relationsdatabaser stöder kolumner som innehåller JSON-dokument. JSON i dessa kolumner kan granskas med frågor. Detta gör det till exempel möjligt att filtrera och sortera efter elementen i dokumenten, samt projektion av element ur dokumenten till resultat. JSON-kolumner gör det möjligt för relationsdatabaser att ta på sig några av egenskaperna hos dokumentdatabaser, vilket skapar en användbar hybrid mellan de två.
EF7 innehåller provideragnostiskt stöd för JSON-kolumner med en implementering för SQL Server. Det här stödet tillåter mappning av aggregeringar som skapats från .NET-typer till JSON-dokument. Normala LINQ-frågor kan användas i aggregeringarna, och dessa översätts till lämpliga frågekonstruktioner som behövs för att öka detaljnivån i JSON. EF7 stöder också uppdatering och sparande av ändringar i JSON-dokument.
Anmärkning
SQLite-stöd för JSON planeras för efter EF7. PostgreSQL- och Pomelo MySQL-leverantörerna innehåller redan stöd för JSON-kolumner. Vi kommer att arbeta med författarna till dessa leverantörer för att anpassa JSON-supporten mellan alla leverantörer.
Mappa till JSON-kolumner
I EF Core definieras aggregeringstyper med hjälp av OwnsOne och OwnsMany. Tänk till exempel på aggregeringstypen från vår exempelmodell som används för att lagra kontaktinformation:
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; }
}
Detta kan sedan användas i entitetstypen "ägare", till exempel för att lagra kontaktuppgifter för en författare:
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ContactDetails Contact { get; set; }
}
Aggregeringstypen konfigureras i OnModelCreating med OwnsOne.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Tips/Råd
Koden som visas här kommer från JsonColumnsSample.cs.
Som standard mappar relationsdatabasprovidrar aggregeringstyper som denna till samma tabell som den ägande entitetstypen. Det vill: varje egenskap för ContactDetails klasserna och Address mappas till en kolumn i Authors tabellen.
Några sparade författare med kontaktuppgifter ser ut så här:
Författare
| Id | Namn | Kontakt_Adress_Gata | Kontakt_Adress_Stad | Kontakt_Adress_Postnummer | Kontakt_Adress_Land | Kontakt_Telefon |
|---|---|---|---|---|---|---|
| 1 | Maddy Montaquila | 1 Main Street | Camberwick Grön | CW1 5ZH | Storbritannien | 01632 12345 |
| 2 | Jeremy Likness | 2 Huvudgatan | Chigley | CW1 5ZH | Storbritannien | 01632 12346 |
| 3 | Daniel Roth | 3 Maingatan | Camberwick Grön | CW1 5ZH | Storbritannien | 01632 12347 |
| 4 | Arthur Vickers | 15a Main St | Chigley | CW1 5ZH | Storbritannien | 01632 22345 |
| 5 | Brice Lambson | 4 Main St | Chigley | CW1 5ZH | Storbritannien | 01632 12349 |
Om du vill kan varje entitetstyp som utgör aggregeringen mappas till en egen tabell i stället:
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");
});
});
}
Samma data lagras sedan i tre tabeller:
Författare
| Id | Namn |
|---|---|
| 1 | Maddy Montaquila |
| 2 | Jeremy Likness |
| 3 | Daniel Roth |
| 4 | Arthur Vickers |
| 5 | Brice Lambson |
Kontakter
| AuthorId | Telefon |
|---|---|
| 1 | 01632 12345 |
| 2 | 01632 12346 |
| 3 | 01632 12347 |
| 4 | 01632 22345 |
| 5 | 01632 12349 |
Adresser
| KontaktinformationFörfattarId | Gata | Stad | Postnummer | Land |
|---|---|---|---|---|
| 1 | 1 Main Street | Camberwick Grön | CW1 5ZH | Storbritannien |
| 2 | 2 Huvudgatan | Chigley | CW1 5ZH | Storbritannien |
| 3 | 3 Maingatan | Camberwick Grön | CW1 5ZH | Storbritannien |
| 4 | 15a Main St | Chigley | CW1 5ZH | Storbritannien |
| 5 | 4 Main St | Chigley | CW1 5ZH | Storbritannien |
Nu, för den intressanta delen. I EF7 kan aggregeringstypen ContactDetails mappas till en JSON-kolumn. Detta kräver bara ett anrop till ToJson() när du konfigurerar aggregeringstypen:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToJson();
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Tabellen Authors kommer nu att innehålla en JSON-kolumn för ContactDetails som fylls med ett JSON-dokument för varje författare.
Författare
| Id | Namn | Kontakt |
|---|---|---|
| 1 | Maddy Montaquila | { "Telefon":"01632 12345", "Adress": { "Stad":"Camberwick Green" "Land":"Storbritannien", "Postnummer":"CW1 5ZH", "Gata":"1 Main St" } } |
| 2 | Jeremy Likness | { "Telefon":"01632 12346", "Adress": { "Stad":"Chigley", "Land":"Storbritannien", "Postnummer":"CH1 5ZH", "Gata":"2 Huvudgatan" } } |
| 3 | Daniel Roth | { "Telefon":"01632 12347", "Adress": { "Stad":"Camberwick Green" "Land":"Storbritannien", "Postnummer":"CW1 5ZH", "Gata":"3 Main St" } } |
| 4 | Arthur Vickers | { "Telefon":"01632 12348", "Adress": { "Stad":"Chigley", "Land":"Storbritannien", "Postnummer":"CH1 5ZH", "Gata":"15a Huvudgatan" } } |
| 5 | Brice Lambson | { "Telefon":"01632 12349", "Adress": { "Stad":"Chigley", "Land":"Storbritannien", "Postnummer":"CH1 5ZH", "Gata":"4 Main St" } } |
Tips/Råd
Den här användningen av aggregeringar liknar hur JSON-dokument mappas när du använder EF Core-providern för Azure Cosmos DB. JSON-kolumner ger funktionerna för att använda EF Core mot dokumentdatabaser till dokument som är inbäddade i en relationsdatabas.
JSON-dokumenten som visas ovan är mycket enkla, men den här mappningsfunktionen kan också användas med mer komplexa dokumentstrukturer. Överväg till exempel en annan aggregeringstyp från vår exempelmodell som används för att representera metadata om ett inlägg:
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; }
}
Den här samlingstypen innehåller flera kapslade typer och samlingar. Anrop till OwnsOne och OwnsMany används för att mappa den här samlingstypen:
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));
});
Tips/Råd
ToJson behövs bara på aggregeringsroten för att mappa hela aggregeringen till ett JSON-dokument.
Med den här mappningen kan EF7 skapa och fråga i ett komplext JSON-dokument så här:
{
"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"
}
]
}
]
}
Anmärkning
Det finns ännu inte stöd för att mappa rumsliga typer direkt till JSON. Dokumentet ovan använder double värden som en lösning. Rösta på Stöd spatiala typer i JSON-kolumner om detta är något du är intresserad av.
Anmärkning
Det finns ännu inte stöd för att mappa samlingar av primitiva typer till JSON. Dokumentet ovan använder en värdekonverterare för att omvandla samlingen till en kommaavgränsad sträng. Rösta på Json: lägg till stöd för insamling av primitiva typer om detta är något du är intresserad av.
Anmärkning
Mappning av ägda typer till JSON stöds ännu inte i samband med TPT- eller TPC-arv. Rösta på Stöd för JSON-egenskaper med TPT/TPC-arvsmappning om detta är något du är intresserad av.
Frågor till JSON-kolumner
Frågor till JSON-kolumner fungerar precis på samma sätt som att fråga till andra aggregeringstyper i EF Core. Det vill säga, använd bara LINQ! Här följer några exempel.
En fråga för alla författare som finns i Chigley:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
Den här frågan genererar följande SQL när du använder 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'
Observera användningen av JSON_VALUE för att hämta City inifrån Address JSON-dokumentet.
Select kan användas för att extrahera och projektelement från JSON-dokumentet:
var postcodesInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.Select(author => author.Contact.Address.Postcode)
.ToListAsync();
Den här frågan genererar följande 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'
Här är ett exempel som gör lite mer i filtret och i projektionen, och som även sorterar efter telefonnummer i JSON-dokumentet.
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();
Den här frågan genererar följande 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))
Och när JSON-dokumentet innehåller samlingar kan dessa projiceras ut i resultatet:
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();
Den här frågan genererar följande 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
Anmärkning
Mer komplexa frågor som rör JSON-samlingar kräver jsonpath stöd. Rösta på stöd för jsonpath-frågeställningar om detta är något du är intresserad av.
Tips/Råd
Överväg att skapa index för att förbättra frågeprestanda i JSON-dokument. Se till exempel Indexera Json-data när du använder SQL Server.
Uppdatera JSON-kolumner
SaveChanges och SaveChangesAsync arbetar på vanligt sätt för att göra uppdateringar av en JSON-kolumn. För omfattande ändringar uppdateras hela dokumentet. Du kan till exempel ersätta det Contact mesta av dokumentet för en författare:
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();
I det här fallet skickas hela det nya dokumentet som en parameter:
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']
Som sedan används i UPDATE SQL:
UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Men om endast ett underdokument ändras använder EF Core ett JSON_MODIFY kommando för att endast uppdatera underdokumentet. Till exempel att ändra Address inuti ett Contact-dokument:
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
await context.SaveChangesAsync();
Genererar följande parametrar:
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']
Som används i UPDATE via ett JSON_MODIFY anrop:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;
Om bara en enskild egenskap ändras använder EF Core slutligen återigen kommandot "JSON_MODIFY", den här gången för att endast korrigera det ändrade egenskapsvärdet. Till exempel:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
Genererar följande parametrar:
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']
Vilka åter används med en JSON_MODIFY:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
ExecuteUpdate och ExecuteDelete (stora uppdateringar)
Som standard spårar EF Core ändringar i entiteter och skickar sedan uppdateringar till databasen när en av SaveChanges metoderna anropas. Ändringar skickas endast för egenskaper och relationer som faktiskt har ändrats. Dessutom är de spårade entiteterna fortfarande synkroniserade med de ändringar som skickas till databasen. Den här mekanismen är ett effektivt och bekvämt sätt att skicka allmänna infogningar, uppdateringar och borttagningar till databasen. Dessa ändringar samlas också för att minska antalet databasanrop.
Ibland kan det dock vara bra att köra uppdaterings- eller borttagningskommandon i databasen utan att använda ändringsspåraren. EF7 aktiverar detta med de nya ExecuteUpdate metoderna och ExecuteDelete metoderna. Dessa metoder tillämpas på en LINQ-fråga och uppdaterar eller tar bort entiteter i databasen baserat på resultatet av frågan. Många entiteter kan uppdateras med ett enda kommando och entiteterna läses inte in i minnet, vilket innebär att detta kan resultera i effektivare uppdateringar och borttagningar.
Tänk dock på att:
- De specifika ändringar som ska göras måste anges uttryckligen. de identifieras inte automatiskt av EF Core.
- Alla spårade entiteter kommer inte att hållas synkroniserade.
- Ytterligare kommandon kan behöva skickas i rätt ordning för att inte bryta mot databasbegränsningar. Du kan till exempel ta bort underordnade innan ett huvudobjekt kan tas bort.
Allt detta innebär att ExecuteUpdate metoderna och ExecuteDelete kompletterar, snarare än ersätter, den befintliga SaveChanges mekanismen.
Grundläggande ExecuteDelete exempel
Tips/Råd
Koden som visas här kommer från ExecuteDeleteSample.cs.
Att anropa ExecuteDelete eller ExecuteDeleteAsync på en DbSet tar omedelbart bort alla entiteter från den DbSet i databasen. Om du till exempel vill ta bort alla Tag entiteter:
await context.Tags.ExecuteDeleteAsync();
Detta kör följande SQL när du använder SQL Server:
DELETE FROM [t]
FROM [Tags] AS [t]
Mer intressant är att frågan kan innehålla ett filter. Till exempel:
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();
Detta kör följande SQL:
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'
Frågan kan också använda mer komplexa filter, inklusive navigering till andra typer. Om du till exempel bara vill ta bort taggar från gamla blogginlägg:
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();
Vilket utför:
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))
Grundläggande ExecuteUpdate exempel
Tips/Råd
Koden som visas här kommer från ExecuteUpdateSample.cs.
ExecuteUpdate och ExecuteUpdateAsync beter sig på ett mycket liknande sätt som ExecuteDelete metoderna. Den största skillnaden är att en uppdatering kräver att du vet vilka egenskaper som ska uppdateras och hur du uppdaterar dem. Detta uppnås med hjälp av ett eller flera anrop till SetProperty. Om du till exempel vill uppdatera Name för varje blogg:
await context.Blogs.ExecuteUpdateAsync(
s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));
Den första parametern SetProperty i anger vilken egenskap som ska uppdateras. I det här fallet Blog.Name. Den andra parametern anger hur det nya värdet ska beräknas. i det här fallet genom att ta det befintliga värdet och lägga till "*Featured!*". Den resulterande SQL:en är:
UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]
Precis som med ExecuteDeletekan frågan användas för att filtrera vilka entiteter som uppdateras. Dessutom kan flera anrop till SetProperty användas för att uppdatera fler än en egenskap på målentiteten. Om du till exempel vill uppdatera Title och Content för alla inlägg som publicerats före 2022:
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 + ")"));
I det här fallet är den genererade SQL:en lite mer komplicerad:
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
Slutligen, återigen som med ExecuteDelete, kan filtret referera till andra tabeller. Om du till exempel vill uppdatera alla taggar från gamla inlägg:
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
Vilket genererar:
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))
Mer information och kodexempel på ExecuteUpdate och ExecuteDeletefinns i ExecuteUpdate och ExecuteDelete.
Arv och flera tabeller
ExecuteUpdate och ExecuteDelete kan bara agera på en enda tabell. Detta får konsekvenser när du arbetar med olika strategier för arvsmappning. I allmänhet finns det inga problem när du använder TPH-mappningsstrategin, eftersom det bara finns en tabell att ändra. Du kan till exempel ta bort alla FeaturedPost entiteter:
await context.Set<FeaturedPost>().ExecuteDeleteAsync();
Genererar följande SQL när du använder TPH-mappning:
DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'
Det finns inte heller några problem för det här fallet när du använder TPC-mappningsstrategin, eftersom det återigen bara behövs ändringar i en enda tabell:
DELETE FROM [f]
FROM [FeaturedPosts] AS [f]
Om du försöker göra detta när du använder TPT-mappningsstrategin misslyckas det, eftersom det skulle kräva att rader tas bort från två olika tabeller.
Att lägga till ett filter i frågan innebär ofta att åtgärden misslyckas med både TPC- och TPT-strategierna. Detta beror återigen på att raderna kan behöva tas bort från flera tabeller. Till exempel den här frågan:
await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();
Genererar följande SQL när du använder 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%')
Misslyckas dock när man använder TPC eller TPT.
Tips/Råd
Problem #10879 följer upp tillägget av stöd för automatisk sändning av flera kommandon i dessa scenarier. Rösta på den här frågan om det är något du vill se implementerat.
ExecuteDelete och relationer
Som nämnts ovan kan det vara nödvändigt att ta bort eller uppdatera beroende entiteter innan huvudnamnet för en relation kan tas bort. Varje Post beror till exempel på sin associerade Author. Det innebär att en författare inte kan tas bort om ett inlägg fortfarande refererar till det. Om du gör det bryter du mot begränsningen för sekundärnyckeln i databasen. Du kan till exempel försöka göra följande:
await context.Authors.ExecuteDeleteAsync();
Resulterar i följande undantag på SQL Server:
Microsoft.Data.SqlClient.SqlException (0x80131904): DELETE-instruktionen stred mot REFERENS-villkoret "FK_Posts_Authors_AuthorId". Konflikten inträffade i databasen "TphBlogsContext", tabellen "dbo. Inlägg", kolumnen "AuthorId". Uttalandet har avslutats.
För att åtgärda detta måste vi först antingen ta bort inläggen eller bryta relationen mellan varje inlägg och dess författare genom att ange AuthorId egenskapen sekundärnyckel till null. Du kan till exempel använda borttagningsalternativet:
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
Tips/Råd
TagWith kan användas för att tagga ExecuteDelete eller ExecuteUpdate på samma sätt som det taggar vanliga frågor.
Detta resulterar i två separata kommandon; den första som tar bort de beroende:
-- Deleting posts...
DELETE FROM [p]
FROM [Posts] AS [p]
Och den andra för att ta bort rektorerna:
-- Deleting authors...
DELETE FROM [a]
FROM [Authors] AS [a]
Viktigt!
Flera ExecuteDelete kommandon och ExecuteUpdate kommer inte att finnas i en enda transaktion som standard.
DbContext-transaktions-API:er kan dock användas på normalt sätt för att omsluta dessa kommandon i en transaktion.
Tips/Råd
Om du skickar dessa kommandon i en enda tur och retur-resa beror det på problem nr 10879. Rösta på den här frågan om det är något du vill se implementerat.
Det kan vara mycket användbart att konfigurera kaskadborttagningar i databasen här. I vår modell krävs relationen mellan Blog och Post , vilket gör att EF Core konfigurerar en kaskadborttagning efter konvention. Det innebär att när en blogg tas bort från databasen tas även alla dess beroende inlägg bort. Det följer sedan att för att ta bort alla bloggar och inlägg behöver vi bara ta bort bloggarna:
await context.Blogs.ExecuteDeleteAsync();
Detta resulterar i följande SQL:
DELETE FROM [b]
FROM [Blogs] AS [b]
Vilket, eftersom den tar bort en blogg, också gör att alla relaterade inlägg tas bort av den konfigurerade kaskadborttagningen.
Snabbare SparaÄndringar
I EF7 har prestandan för SaveChanges och SaveChangesAsync förbättrats avsevärt. I vissa scenarier är det nu upp till fyra gånger snabbare att spara ändringar än med EF Core 6.0!
De flesta av dessa förbättringar kommer från:
- Utföra färre tur och retur-flöden till databasen
- Generera snabbare SQL
Några exempel på dessa förbättringar visas nedan.
Anmärkning
Mer information om dessa ändringar finns i Meddelande om Entity Framework Core 7 Preview 6: Performance Edition på .NET-bloggen.
Tips/Råd
Koden som visas här kommer från SaveChangesPerformanceSample.cs.
Onödiga transaktioner elimineras
Alla moderna relationsdatabaser garanterar transaktionalitet för (de flesta) enskilda SQL-instruktioner. Instruktionen slutförs alltså aldrig bara delvis, även om ett fel inträffar. EF7 undviker att starta en explicit transaktion i dessa fall.
Titta till exempel på loggningen för följande anrop till SaveChanges:
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
Visar att kommandot i EF Core 6.0 INSERT omsluts av kommandon för att starta och sedan genomföra en transaktion:
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.
EF7 identifierar att transaktionen inte behövs här och tar därför bort dessa anrop:
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);
Detta tar bort två databasrundturer, vilket kan göra stor skillnad för övergripande prestanda, särskilt när svarstiden för anrop till databasen är hög. I typiska produktionssystem finns databasen inte på samma dator som programmet. Det innebär att svarstiden ofta är relativt hög, vilket gör den här optimeringen särskilt effektiv i verkliga produktionssystem.
Förbättrad SQL för enkel identitetsinfogning
Fallet ovan infogar en enskild rad med en IDENTITY nyckelkolumn och inga andra databasgenererade värden. EF7 förenklar SQL i det här fallet genom att använda OUTPUT INSERTED. Även om den här förenklingen inte är giltig i många andra fall är det fortfarande viktigt att förbättra eftersom den här typen av infogad enskild rad är mycket vanlig i många program.
Infoga flera rader
I EF Core 6.0 drevs standardmetoden för att infoga flera rader av begränsningar i SQL Server-stöd för tabeller med utlösare. Vi ville se till att standardupplevelsen fungerade även för den minoritet av användare som har utlösare i sina tabeller. Det innebar att vi inte kunde använda en enkel OUTPUTsats eftersom det inte fungerar med utlösare på SQL Server. När du infogar flera entiteter genererade EF Core 6.0 i stället en ganska invecklad SQL. Det här anropet till SaveChanges:
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
Resulterar i följande åtgärder när du kör mot SQL Server med 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.
Viktigt!
Även om detta är komplicerat är batchbearbetning av flera infogningar som detta fortfarande betydligt snabbare än att skicka ett enda kommando för varje infogning.
I EF7 kan du fortfarande hämta den här SQL-filen om tabellerna innehåller utlösare, men i vanliga fall genererar vi nu mycket mer effektiva, om än fortfarande något komplexa, kommandon:
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;
Transaktionen är borta, som i det enskilda infogningsfallet, eftersom MERGE är en enda instruktion som skyddas av en implicit transaktion. Dessutom är den tillfälliga tabellen borta och output-satsen skickar nu de genererade ID:na direkt tillbaka till klienten. Det kan vara fyra gånger snabbare än på EF Core 6.0, beroende på miljöfaktorer som svarstid mellan programmet och databasen.
Utlösare
Om tabellen har utlösare utlöser anropet till SaveChanges i koden ovan ett undantag:
Ohanterat undantag. Microsoft.EntityFrameworkCore.DbUpdateException:
Det gick inte att spara ändringar eftersom måltabellen har databasutlösare. Vänligen konfigurera din entitetstyp i enlighet med detta och sehttps://aka.ms/efcore-docs-sqlserver-save-changes-and-triggersför mer information.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
Måltabellen BlogsWithTriggers för DML-instruktionen kan inte ha några aktiverade utlösare om instruktionen innehåller en OUTPUT-sats utan INTO-satsen.
Följande kod kan användas för att informera EF Core om att tabellen har en utlösare:
modelBuilder
.Entity<BlogWithTrigger>()
.ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));
EF7 återgår sedan till EF Core 6.0 SQL när du skickar infoga och uppdatera kommandon för den här tabellen.
Mer information, inklusive en konvention för att automatiskt konfigurera alla mappade tabeller med utlösare, finns i SQL Server-tabeller med utlösare som nu kräver särskild EF Core-konfiguration i dokumentationen om icke-bakåtkompatibla ändringar i EF7.
Färre tur och retur för att infoga grafer
Överväg att infoga ett diagram över entiteter som innehåller en ny huvudentitet och även nya beroende entiteter med sekundärnycklar som refererar till det nya huvudkontot. Till exempel:
await context.AddAsync(
new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();
Om huvudobjektets primärnyckel genereras av databasen kan värdet för främmande nyckel i det beroende objektet inte vara känt förrän huvudobjektet har infogats. EF Core genererar två rundresor för detta – en för att infoga huvudentiteten och hämta tillbaka den nya primärnyckeln, och en andra för att infoga de beroende entiteterna med värdet för den främmande nyckeln. Och eftersom det finns två instruktioner för detta behövs en transaktion, vilket innebär totalt fyra rundresor.
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.
I vissa fall är dock det primära nyckelvärdet känt innan huvudobjektet infogas. Detta omfattar:
- Nyckelvärden som inte genereras automatiskt
- Nyckelvärden som genereras på klienten, till exempel Guid nycklar
- Nyckelvärden som genereras på servern i batchar, till exempel när du använder en hi-lo värdegenerator
I EF7 optimeras dessa fall nu till en enda tur och retur-resa. I fallet ovan på SQL Server kan till exempel den Blog.Id primära nyckeln konfigureras för att använda hi-lo generationsstrategi:
modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();
Anropet SaveChanges ovanifrån är nu optimerat till en enda tur och retur för infogningarna.
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.
Observera att en transaktion fortfarande behövs här. Det beror på att infogningar görs i två separata tabeller.
EF7 använder också en enda batch i andra fall där EF Core 6.0 skulle skapa fler än en. Till exempel när du tar bort och infogar rader i samma tabell.
Värdet för SaveChanges
Som några av exemplen här visar kan det vara komplicerat att spara resultat i databasen. Det är här som användning av något som EF Core verkligen visar sitt värde. EF Core:
- Samlar flera infognings-, uppdaterings- och borttagningskommandon för att minska antalet nätverksresor.
- Tar reda på om en explicit transaktion behövs eller inte
- Avgör vilken ordning entiteter ska infogas, uppdateras och tas bort så att databasbegränsningarna inte överträds
- Säkerställer att databasgenererade värden returneras effektivt och sprids tillbaka till entiteter
- Ange värden för främmande nycklar automatiskt med hjälp av de värden som genereras för primära nycklar
- Identifiera samtidighetskonflikter
Dessutom kräver olika databassystem olika SQL för många av dessa fall. EF Core-databasprovidern fungerar med EF Core för att säkerställa att korrekta och effektiva kommandon skickas för varje ärende.
Tabell-per-konkrett-typ-arvsmappning (TPC)
Som standard mappar EF Core en arvshierarki med .NET-typer till en enskild databastabell. Detta kallas för TPH-mappningsstrategin (table-per-hierarchy). EF Core 5.0 introducerade TPT-strategin (table-per-type), som stöder mappning av varje .NET-typ till en annan databastabell. EF7 introducerar TPC-strategin (table-per-concrete-type). TPC mappar även .NET-typer till olika tabeller, men på ett sätt som åtgärdar några vanliga prestandaproblem med TPT-strategin.
Tips/Råd
Koden som visas här kommer från TpcInheritanceSample.cs.
Tips/Råd
EF-teamet demonstrerade och talade ingående om TPC-mappning i ett avsnitt av .NET Data Community Standup. Som med alla Community Standup-avsnitt kan du titta på TPC-avsnittet nu på YouTube.
TPC-databasschema
TPC-strategin liknar TPT-strategin förutom att en annan tabell skapas för varje konkret typ i hierarkin, men tabeller skapas inte för abstrakta typer – därav namnet "table-per-concrete-type". Precis som med TPT anger själva tabellen typen av det sparade objektet. Men till skillnad från TPT-mappning innehåller varje tabell kolumner för varje egenskap i betongtypen och dess bastyper. TPC-databasscheman avnormaliseras.
Överväg till exempel att mappa den här hierarkin:
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>"}";
}
När du använder SQL Server är tabellerna som skapats för den här hierarkin:
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]));
Observera att:
Det finns inga tabeller för
AnimalellerPettyper, eftersom dessa ärabstracti objektmodellen. Kom ihåg att C# inte tillåter instanser av abstrakta typer, och det finns därför ingen situation där en abstrakt typinstans sparas i databasen.Mappningen av egenskaper i bastyper upprepas för varje betongtyp. Till exempel har varje tabell en
Namekolumn, och både Katter och Hundar har enVetkolumn.Om du sparar data i den här databasen resulterar det i följande:
Cats-tabell
| Id | Namn | FoodId | Veterinär | Utbildningsnivå |
|---|---|---|---|---|
| 1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Masterexamen i företagsekonomi (MBA) |
| 2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Förskola |
| 8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Hundarnas tabell
| Id | Namn | FoodId | Veterinär | FavoriteToy |
|---|---|---|---|---|
| 3 | Rostat bröd | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Herr Ekorre |
Farmdjurstabell
| Id | Namn | FoodId | Värde | Art |
|---|---|---|---|---|
| 4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus |
Humans-tabell
| Id | Namn | FoodId | FavoritDjurId |
|---|---|---|---|
| 5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
| 6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
| 9 | Katie | noll | 8 |
Observera att, till skillnad från med TPT-mappning, finns all information för ett enskilt objekt i en enda tabell. Och till skillnad från med TPH-mappning finns det ingen kombination av kolumn och rad i en tabell där den aldrig används av modellen. Nedan ser vi hur dessa egenskaper kan vara viktiga för frågor och lagring.
Konfigurera TPC-arv
Alla typer i en arvshierarki måste uttryckligen inkluderas i modellen när hierarkin mappas med EF Core. Detta kan göras genom att skapa DbSet egenskaper på din DbContext för varje typ:
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>();
Eller genom att använda Entity-metoden i OnModelCreating.
modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();
Viktigt!
Detta skiljer sig från det äldre EF6-beteendet, där härledda typer av mappade bastyper identifieras automatiskt om de fanns i samma sammansättning.
Inget annat behöver göras för att mappa hierarkin som TPH, eftersom det är standardstrategin. Från och med EF7 kan TPH dock göras explicit genom att anropa UseTphMappingStrategy bastypen för hierarkin:
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
Om du vill använda TPT i stället ändrar du detta till UseTptMappingStrategy:
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
UseTpcMappingStrategy På samma sätt används för att konfigurera TPC:
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
I varje fall hämtas tabellnamnet som används för varje typ från egenskapsnamnet DbSet på din DbContext, eller så kan det konfigureras med hjälp av -metoden eller ToTable-attributet.
TPC-frågeprestanda
För frågor är TPC-strategin en förbättring jämfört med TPT eftersom den säkerställer att informationen för en viss entitetsinstans alltid lagras i en enda tabell. Det innebär att TPC-strategin kan vara användbar när den mappade hierarkin är stor och har många konkreta (vanligtvis löv) typer, var och en med ett stort antal egenskaper, och där endast en liten delmängd av typerna används i de flesta frågor.
SQL som genereras för tre enkla LINQ-frågor kan användas för att se var TPC fungerar bra jämfört med TPH och TPT. Dessa frågor är:
En fråga som returnerar entiteter av alla typer i hierarkin:
context.Animals.ToList();En fråga som returnerar entiteter från en delmängd av typerna i hierarkin:
context.Pets.ToList();En fråga som endast returnerar entiteter från en enskild lövtyp i hierarkin:
context.Cats.ToList();
TPH-frågor
När du använder TPH utför alla tre frågorna sina förfrågningar mot en enda tabell, men med olika filter på det diskriminerande attributet:
TPH SQL returnerar entiteter av alla typer i hierarkin:
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]TPH SQL returnerar entiteter från en delmängd av typerna i hierarkin:
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')TPH SQL returnerar endast entiteter från en enskild lövtyp i hierarkin:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel] FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Cat'
Alla dessa frågor bör fungera bra, särskilt med ett lämpligt databasindex i den diskriminerande kolumnen.
TPT-frågor
När du använder TPT kräver alla dessa frågor anslutning av flera tabeller, eftersom data för en viss konkret typ delas upp i många tabeller:
TPT SQL returnerar entiteter av alla typer i hierarkin:
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 returnerar entiteter från en delmängd av typerna i hierarkin:
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 returnerar endast entiteter från en enskild lövtyp i hierarkin:
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]
Anmärkning
EF Core använder "diskriminerande syntes" för att avgöra vilken tabell data kommer från och därmed rätt typ att använda. Detta fungerar eftersom LEFT JOIN returnerar nullvärden för den beroende ID-kolumnen ("undertabellerna") som inte är rätt typ. Så för en hund kommer [d].[Id] att vara icke-null, och alla andra konkreta ID:n kommer att vara null.
Alla dessa sökfrågor kan drabbas av prestandaproblem på grund av tabellkopplingarna. Därför är TPT aldrig ett bra val för frågeprestanda.
TPC-frågor
TPC förbättras jämfört med TPT för alla dessa frågor eftersom antalet tabeller som behöver frågas minskar. Dessutom kombineras resultaten från varje tabell med hjälp av UNION ALL, vilket kan vara betydligt snabbare än en tabellkoppling, eftersom den inte behöver utföra någon matchning mellan rader eller deduplicering av rader.
TPC SQL returnerar entiteter av alla typer i hierarkin:
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 returnerar entiteter från en delmängd av typerna i hierarkin:
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 returnerar endast entiteter från en enskild lövtyp i hierarkin:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel] FROM [Cats] AS [c]
Även om TPC är bättre än TPT för alla dessa frågor är TPH-frågorna fortfarande bättre när instanser av flera typer returneras. Detta är en av anledningarna till att TPH är standardstrategin som används av EF Core.
Som SQL för fråga nr 3 visar utmärker sig TPC verkligen när du frågar efter entiteter av en enskild lövtyp. Frågan använder bara en enskild tabell och behöver ingen filtrering.
TPC-infogningar och uppdateringar
TPC fungerar också bra när du sparar en ny entitet, eftersom detta kräver att endast en enskild rad infogas i en enda tabell. Detta gäller även för TPH. Med TPT måste rader infogas i många tabeller, vilket presterar mindre bra.
Detsamma gäller ofta för uppdateringar, men i det här fallet om alla kolumner som uppdateras finns i samma tabell, även för TPT, kanske skillnaden inte är betydande.
Utrymmesöverväganden
Både TPT och TPC kan använda mindre lagring än TPH när det finns många undertyper med många egenskaper som ofta inte används. Det beror på att varje rad i TPH-tabellen måste lagra en för var och en NULL av dessa oanvända egenskaper. I praktiken är detta sällan ett problem, men det kan vara värt att tänka på när du lagrar stora mängder data med dessa egenskaper.
Tips/Råd
Om databassystemet stöder det (e.g. SQL Server) bör du överväga att använda "glesa kolumner" för TPH-kolumner som sällan fylls i.
Nyckelgenerering
Den valda arvsmappningsstrategin får konsekvenser för hur primära nyckelvärden genereras och hanteras. Nycklar i TPH är enkla eftersom varje entitetsinstans representeras av en enda rad i en enda tabell. Alla typer av nyckelvärdegenerering kan användas och inga ytterligare begränsningar behövs.
För TPT-strategin finns det alltid en rad i tabellen som mappas till bastypen för hierarkin. Alla typer av generering av nycklar kan användas på den här tabellen, och nycklarna för andra tabeller är länkade till denna tabell med hjälp av begränsningar för främmande nyckel.
Saker blir lite mer komplicerade för TPC. För det första är det viktigt att förstå att EF Core kräver att alla entiteter i en hierarki måste ha ett unikt nyckelvärde, även om entiteterna har olika typer. Med vår exempelmodell kan en hund därför inte ha samma ID-nyckelvärde som en katt. För det andra finns det, till skillnad från TPT, ingen gemensam tabell som kan fungera som den enda plats där nyckelvärdena finns och kan genereras. Det innebär att det inte går att använda en enkel Identity kolumn.
För databaser som stöder sekvenser kan nyckelvärden genereras med hjälp av en enda sekvens som refereras till i standardvillkoret för varje tabell. Det här är den strategi som används i de TPC-tabeller som visas ovan, där varje tabell har följande:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence är en databassekvens som skapats av EF Core. Den här strategin används som standard för TPC-hierarkier när du använder EF Core-databasprovidern för SQL Server. Databasprovidrar för andra databaser som stöder sekvenser bör ha ett liknande standardvärde. Andra nyckelgenereringsstrategier som använder sekvenser, till exempel Hi-Lo-mönster, kan också användas med TPC.
Standardidentitetskolumner fungerar inte med TPC, men det är möjligt att använda identitetskolumner om varje tabell har konfigurerats med ett lämpligt startvärde och öka så att värdena som genereras för varje tabell aldrig hamnar i konflikt. Till exempel:
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 stöder inte sekvenser eller identitetsutsäde/inkrement, och därför stöds inte heltalsnyckelvärdegenerering när du använder SQLite med TPC-strategin. Men generering på klientsidan eller globalt unika nycklar – till exempel GUID-nycklar – stöds på alla databaser, inklusive SQLite.
Begränsningar för främmande nyckel
TPC-mappningsstrategin skapar ett avnormaliserat SQL-schema – det här är en anledning till att vissa databaspurister är emot det. Anta till exempel kolumnen för foreign key FavoriteAnimalId. Värdet i den här kolumnen måste matcha det primära nyckelvärdet för vissa djur. Detta kan tillämpas i databasen med en enkel FK-begränsning när du använder TPH eller TPT. Till exempel:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Men när du använder TPC lagras den primära nyckeln för ett djur i tabellen för den konkreta typen av det djuret. Till exempel lagras en katts primärnyckel i kolumnen Cats.Id, medan en hunds primärnyckel lagras i kolumnen Dogs.Id och så vidare. Det innebär att det inte går att skapa en FK-begränsning för den här relationen.
I praktiken är detta inte ett problem så länge programmet inte försöker infoga ogiltiga data. Om alla data till exempel infogas av EF Core och använder navigeringar för att relatera entiteter är det garanterat att FK-kolumnen alltid innehåller ett giltigt PK-värde.
Sammanfattning och vägledning
Sammanfattningsvis är TPC en bra mappningsstrategi att använda när koden främst frågar efter entiteter av en enda lövtyp. Det beror på att lagringskraven är mindre och det inte finns någon diskriminerande kolumn som kan behöva ett index. Infogningar och uppdateringar är också effektiva.
Med detta sagt är TPH vanligtvis bra för de flesta program och är en bra standard för en mängd olika scenarier, så lägg inte till komplexiteten i TPC om du inte behöver det. Mer specifikt, om koden främst frågar efter entiteter av många typer, till exempel att skriva frågor mot bastypen, lutar du dig mot TPH över TPC.
Använd endast TPT om du är tvingad till det av externa faktorer.
Anpassade bakåtkompileringsmallar
Nu kan du anpassa den genererade koden vid omvänd konstruktion av en EF-modell från en databas. Kom igång genom att lägga till standardmallarna i projektet:
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
Mallarna kan sedan anpassas och används automatiskt av dotnet ef dbcontext scaffold och Scaffold-DbContext.
Mer information finns i Anpassade bakåtkompileringsmallar.
Tips/Råd
EF-teamet demonstrerade och talade ingående om omvända tekniska mallar i ett avsnitt av .NET Data Community Standup. Precis som med alla Community Standup-avsnitt kan du titta på avsnittet T4-mallar nu på YouTube.
Konventioner för modellbyggande
EF Core använder en metadatamodell för att beskriva hur programmets entitetstyper mappas till den underliggande databasen. Den här modellen skapas med hjälp av en uppsättning med cirka 60 "konventioner". Modellen som skapats av konventioner kan sedan anpassas med hjälp av mappningsattribut (även kallade "dataanteckningar") och/eller anrop till API:et DbModelBuilder i OnModelCreating.
Från och med EF7 kan program nu ta bort eller ersätta någon av dessa konventioner samt lägga till nya konventioner. Modellbyggkonventioner är ett kraftfullt sätt att styra modellkonfigurationen, men kan vara komplext och svårt att få rätt. I många fall kan den befintliga förkonventa modellkonfigurationen användas i stället för att enkelt ange en gemensam konfiguration för egenskaper och typer.
Ändringar i de konventioner som används av en DbContext görs genom att åsidosätta DbContext.ConfigureConventions-metoden. Till exempel:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Tips/Råd
Om du vill hitta alla inbyggda modellbyggkonventioner letar du efter varje klass som implementerar IConvention gränssnittet.
Tips/Råd
Koden som visas här kommer från ModelBuildingConventionsSample.cs.
Ta bort en befintlig konvention
Ibland kanske en av de inbyggda konventionerna inte är lämplig för ditt program, i vilket fall det kan tas bort.
Exempel: Skapa inte index för kolumner för främmande nycklar
Det är vanligtvis klokt att skapa index för FK-kolumner (sekundärnyckel) och därför finns det en inbyggd konvention för detta: ForeignKeyIndexConvention. Om du tittar på modellfelsökningsvyn för en entitetstyp Post med relationer till Blog och Authorkan vi se att två index har skapats – ett för BlogId FK och det andra för AuthorId 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
Index har dock omkostnader, och som du frågar här kanske det inte alltid är lämpligt att skapa dem för alla FK-kolumner. För att uppnå detta ForeignKeyIndexConvention kan du ta bort när du skapar modellen:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Om vi tittar på felsökningsvyn för modellen för Post nu, ser vi att indexen på FK:er inte har skapats.
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
När det är önskvärt kan index fortfarande skapas uttryckligen för utländska nyckelkolumner, antingen med hjälp av IndexAttribute:
[Index("BlogId")]
public class Post
{
// ...
}
Eller med inställningar i OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}
Om du tittar på Post entitetstypen igen innehåller den nu indexet BlogId , men inte indexet AuthorId :
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
Tips/Råd
Om din modell inte använder mappningsattribut (även kallade dataanteckningar) för konfiguration kan alla konventioner som slutar med AttributeConvention tas bort på ett säkert sätt för att påskynda modellskapandet.
Lägga till en ny konvention
Att ta bort befintliga konventioner är en början, men hur är det med att lägga till helt nya modellbyggkonventioner? EF7 stöder detta också!
Exempel: Begränsa längden på diskriminerande egenskaper
Strategin för arvsmappning i tabell-per-hierarki kräver en diskriminerande kolumn för att ange vilken typ som representeras på en viss rad. Som standardinställning använder EF en obegränsad strängkolumn för diskriminatorn, vilket säkerställer att den fungerar för alla diskriminatorlängder. Om du begränsar den maximala längden på diskriminerande strängar kan det dock ge effektivare lagring och frågor. Nu ska vi skapa en ny konvention som gör det.
EF Core-modellbyggkonventioner utlöses baserat på ändringar som görs i modellen när den byggs. Detta behåller modellen up-to-date när explicit konfiguration görs, mappningsattribut tillämpas och andra konventioner körs. För att delta i detta implementerar varje konvention ett eller flera gränssnitt som avgör när konventionen ska utlösas. En konvention som implementerar IEntityTypeAddedConvention utlöses till exempel när en ny entitetstyp läggs till i modellen. På samma sätt utlöses en konvention som implementerar båda IForeignKeyAddedConvention och IKeyAddedConvention när antingen en nyckel eller en sekundärnyckel läggs till i modellen.
Det kan vara svårt att veta vilka gränssnitt som ska implementeras, eftersom konfigurationen av modellen vid ett tillfälle kan ändras eller tas bort vid ett senare tillfälle. En nyckel kan till exempel skapas av konventionen, men sedan ersättas senare när en annan nyckel har konfigurerats explicit.
Låt oss göra detta lite mer konkret genom att göra ett första försök att implementera konventionen om diskriminerande längd:
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);
}
}
}
Den här konventionen implementerar IEntityTypeBaseTypeChangedConvention, vilket innebär att den utlöses när den mappade arvshierarkin för en entitetstyp ändras. Konventionen hittar och konfigurerar sedan strängdiskrimineringsegenskapen för hierarkin.
Den här konventionen används sedan genom att anropa Add i ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DiscriminatorLengthConvention1());
}
Tips/Råd
I stället för att lägga till en instans av konventionen direkt accepterar Add metoden en fabrik för att skapa instanser av konventionen. Detta gör att konventionen kan använda beroenden från den interna EF Core-tjänstleverantören. Eftersom den här konventionen inte har några beroenden heter _parametern för tjänstprovidern , vilket indikerar att den aldrig används.
Att skapa modellen och titta på Post entitetstypen visar att detta har fungerat – den diskriminerande egenskapen är nu konfigurerad till med en maximal längd på 24:
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Men vad händer om vi nu uttryckligen konfigurerar en annan diskriminerande egenskap? Till exempel:
modelBuilder.Entity<Post>()
.HasDiscriminator<string>("PostTypeDiscriminator")
.HasValue<Post>("Post")
.HasValue<FeaturedPost>("Featured");
När vi tittar på felsökningsvyn för modellen upptäcker vi att den diskriminerande längden inte längre är konfigurerad!
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw
Detta beror på att diskrimineringsegenskapen som vi konfigurerade i vår konvention senare togs bort när den anpassade diskriminatorn lades till. Vi skulle kunna försöka åtgärda detta genom att implementera ett annat gränssnitt i vår konvention för att reagera på de diskriminerande förändringarna, men det är inte lätt att ta reda på vilket gränssnitt som ska implementeras.
Lyckligtvis finns det ett annat sätt att närma sig detta som gör saker mycket enklare. Mycket av tiden spelar det ingen roll hur modellen ser ut när den byggs, så länge den slutliga modellen är korrekt. Dessutom behöver den konfiguration som vi vill tillämpa ofta inte utlösa andra konventioner för att reagera. Därför kan vår konvention implementera IModelFinalizingConvention. Modellslutkonventioner körs när alla andra modellbyggen har slutförts och har därför åtkomst till modellens slutliga tillstånd. En modell som slutför konventionen itererar vanligtvis över hela modellen och konfigurerar modellelement allt eftersom. Så i det här fallet hittar vi alla diskriminerande i modellen och konfigurerar den:
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);
}
}
}
}
När vi har skapat modellen med den här nya konventionen konstaterar vi att diskriminatorns längd nu är korrekt konfigurerad även om den har anpassats.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
För skojs skull ska vi gå ett steg längre och konfigurera maxlängden så att den är längden på det längsta diskriminerande värdet.
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);
}
}
}
}
Nu är den maximala längden för diskriminator-kolumnen 8, vilket är längden på "Utvald", det längsta diskriminatorvärdet som används.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
Tips/Råd
Du kanske undrar om konventionen också ska skapa ett index för den diskriminerande kolumnen. Det finns en diskussion om detta på GitHub. Det korta svaret är att ibland kan ett index vara användbart, men för det mesta kommer det förmodligen inte att vara det. Därför är det bäst att skapa lämpliga index här efter behov, i stället för att ha en konvention för att göra det alltid. Men om du inte håller med om detta kan konventionen ovan enkelt ändras för att skapa ett index också.
Exempel: Standardlängd för alla strängegenskaper
Nu ska vi titta på ett annat exempel där en avslutande konvention kan användas – den här gången anger vi en maximal standardlängd för alla strängegenskaper, som efterfrågas på GitHub.Let's look at another example where a finalizing convention can be used --this time, setting a default maximum length for any string property, as asked for on GitHub. Konventionen ser ut ungefär som i föregående exempel:
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);
}
}
}
Den här konventionen är ganska enkel. Den hittar varje strängegenskap i modellen och anger maxlängden till 512. När vi tittar i felsökningsvyn på egenskaperna för Postser vi att alla strängegenskaper nu har en maxlängd på 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)
Men egenskapen Content bör förmodligen tillåta mer än 512 tecken, eller alla våra inlägg kommer att vara ganska kort! Detta kan göras utan att ändra vår konvention genom att uttryckligen konfigurera maxlängden för just den här egenskapen, antingen med hjälp av ett mappningsattribut:
[MaxLength(4000)]
public string Content { get; set; }
Eller med kod i OnModelCreating:
modelBuilder.Entity<Post>()
.Property(post => post.Content)
.HasMaxLength(4000);
Nu har alla egenskaper en maxlängd på 512, förutom Content vilken uttryckligen konfigurerades med 4 000:
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)
Så varför åsidosätter inte vår konvention den uttryckligen konfigurerade maxlängden? Svaret är att EF Core håller reda på hur varje konfiguration gjordes. Detta representeras av ConfigurationSource enumerationen. De olika typerna av konfiguration är:
-
Explicit: Modellelementet konfigurerades uttryckligen iOnModelCreating -
DataAnnotation: Modellelementet konfigurerades med hjälp av ett mappningsattribut (även kallat dataanteckning) för CLR-typen -
Convention: Modellelementet konfigurerades av en modellbyggnadskonvention
Konventioner åsidosätter aldrig konfiguration som markerats som DataAnnotation eller Explicit. Detta uppnås med hjälp av en "convention builder", till exempel IConventionPropertyBuilder, som hämtas från egenskapen Builder. Till exempel:
property.Builder.HasMaxLength(512);
Om du anropar HasMaxLength convention builder anges bara maxlängden om den inte redan har konfigurerats av ett mappningsattribut eller i OnModelCreating.
Builder-metoder som denna har också en andra parameter: fromDataAnnotation. Ställ in detta till true om konventionen utför konfigurationen för ett mappningsattribut. Till exempel:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
Detta ställer in ConfigurationSource till DataAnnotation, vilket innebär att värdet nu kan åsidosättas genom explicit mappning på OnModelCreating, men inte av konventioner för attribut som inte mappas.
Slutligen, innan vi lämnar det här exemplet, vad händer om vi använder både MaxStringLengthConvention och DiscriminatorLengthConvention3 samtidigt? Svaret är att det beror på vilken ordning de läggs till, eftersom modellslutkonventioner körs i den ordning de läggs till. Så om MaxStringLengthConvention läggs till sist kommer den att köras sist, och den kommer att ange den maximala längden på den diskriminerande egenskapen till 512. I det här fallet är det därför bättre att lägga DiscriminatorLengthConvention3 till sist så att den kan åsidosätta den maximala standardlängden för bara diskriminerande egenskaper, samtidigt som alla andra strängegenskaper lämnas kvar som 512.
Ersätta en befintlig konvention
Ibland i stället för att ta bort en befintlig konvention helt vill vi i stället ersätta den med en konvention som gör i princip samma sak, men med ändrat beteende. Detta är användbart eftersom den befintliga konventionen redan implementerar de gränssnitt som behövs för att utlösas på rätt sätt.
Exempel: Mappning av opt-in-egenskap
EF Core mappar alla offentliga läs- och skrivbara egenskaper enligt konvention. Detta kanske inte är lämpligt för hur dina entitetstyper definieras. För att ändra detta kan vi ersätta PropertyDiscoveryConvention med vår egen implementering som inte mappar någon egenskap om den inte uttryckligen mappas i OnModelCreating eller markeras med ett nytt attribut med namnet Persist:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}
Här är den nya konventionen:
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;
}
}
}
}
Tips/Råd
När du ersätter en inbyggd konvention bör den nya konventionsimplementeringen ärva från den befintliga konventionsklassen. Observera att vissa konventioner har relations- eller providerspecifika implementeringar, i vilket fall den nya konventionsimplementeringen bör ärva från den mest specifika befintliga konventionsklassen för databasleverantören som används.
Konventionen registreras sedan med metoden Replace i ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}
Tips/Råd
Det här är ett fall där den befintliga konventionen har beroenden som representeras av ProviderConventionSetBuilderDependencies beroendeobjektet. Dessa hämtas från den interna tjänstleverantören med hjälp av GetRequiredService och skickas till konventionskonstruktorn.
Den här konventionen fungerar genom att hämta alla läsbara egenskaper och fält från den angivna entitetstypen. Om medlemmen tillskrivs [Persist], mappas den genom att anrop görs:
entityTypeBuilder.Property(memberInfo);
Om medlemmen å andra sidan är en egenskap som annars skulle ha mappats undantas den från modellen med hjälp av:
entityTypeBuilder.Ignore(propertyInfo.Name);
Observera att den här konventionen tillåter att fält mappas (förutom egenskaper) så länge de är markerade med [Persist]. Det innebär att vi kan använda privata fält som dolda nycklar i modellen.
Tänk till exempel på följande entitetstyper:
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; }
}
Modellen som skapats av dessa entitetstyper är:
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
Observera att vanligtvis IsClean skulle ha kartlagts, men eftersom det inte är markerat med [Persist] (förmodligen eftersom renlighet inte är en beständig egenskap för tvätt), behandlas den nu som en omarkerad egenskap.
Tips/Råd
Det gick inte att implementera den här konventionen som en modell för att slutföra konventionen eftersom mappning av en egenskap utlöser många andra konventioner som ska köras för att ytterligare konfigurera den mappade egenskapen.
Mappning av lagrad procedur
Som standard genererar EF Core kommandon för att infoga, uppdatera och ta bort som fungerar direkt med tabeller eller uppdateringsbara vyer. EF7 introducerar stöd för mappning av dessa kommandon till lagrade procedurer.
Tips/Råd
EF Core har alltid stöd för frågor via lagrade procedurer. Det nya stödet i EF7 handlar uttryckligen om att använda lagrade procedurer för infogningar, uppdateringar och borttagningar.
Viktigt!
Stöd för mappning av lagrade procedurer innebär inte att lagrade procedurer rekommenderas.
Lagrade procedurer mappas med OnModelCreating hjälp av InsertUsingStoredProcedure, UpdateUsingStoredProcedureoch DeleteUsingStoredProcedure. Om du till exempel vill mappa lagrade procedurer för en entitetstyp Person :
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();
});
Den här konfigurationen mappar till följande lagrade procedurer när du använder SQL Server:
För infogningar
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
För uppdateringar
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
För borttagningar
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
Tips/Råd
Lagrade procedurer behöver inte användas för varje typ i modellen eller för alla åtgärder av en viss typ. Om det till exempel bara DeleteUsingStoredProcedure anges för en viss typ genererar EF Core SQL som vanligt för infognings- och uppdateringsåtgärder och använder endast den lagrade proceduren för borttagningar.
Det första argumentet som skickas till varje metod är namnet på den lagrade proceduren. Detta kan utelämnas, i vilket fall EF Core använder tabellnamnet som läggs till med "_Insert", "_Update" eller "_Delete". I exemplet ovan, eftersom tabellen kallas "Personer", kan de lagrade procedurnamnen tas bort utan någon funktionsändring.
Det andra argumentet är en byggare som används för att konfigurera indata och utdata för den lagrade proceduren, inklusive parametrar, returvärden och resultatkolumner.
Parameterar
Parametrar måste läggas till i byggaren i samma ordning som de visas i definitionen för lagrad procedur.
Anmärkning
Parametrar kan namnges, men EF Core anropar alltid lagrade procedurer med hjälp av positionsargument i stället för namngivna argument. Rösta på Tillåt konfiguration av sproc-mappning att använda parameternamn för anrop om anrop efter namn är något som du är intresserad av.
Det första argumentet till varje parameter builder-metod anger egenskapen i modellen som parametern är bunden till. Detta kan vara ett lambda-uttryck:
storedProcedureBuilder.HasParameter(a => a.Name);
Eller en sträng, som är särskilt användbar vid mappning av skuggegenskaper:
storedProcedureBuilder.HasParameter("Name");
Parametrar konfigureras som "indata" som standard. Parametrarna "Output" eller "input/output" kan konfigureras med hjälp av en kapslad byggare. Till exempel:
storedProcedureBuilder.HasParameter(
document => document.RetrievedOn,
parameterBuilder => parameterBuilder.IsOutput());
Det finns tre olika builder-metoder för olika varianter av parametrar:
-
HasParameteranger en normal parameter som är bunden till det aktuella värdet för den angivna egenskapen. -
HasOriginalValueParameteranger en parameter som är bunden till det ursprungliga värdet för den angivna egenskapen. Det ursprungliga värdet är det värde som egenskapen hade när den efterfrågades från databasen, om det är känt. Om det här värdet inte är känt används det aktuella värdet i stället. Ursprungliga värdeparametrar är användbara för samtidighetstoken. -
HasRowsAffectedParameteranger en parameter som används för att returnera antalet rader som påverkas av den lagrade proceduren.
Tips/Råd
Ursprungliga värdeparametrar måste användas för nyckelvärden i "uppdatera" och "ta bort" lagrade procedurer. Detta säkerställer att rätt rad uppdateras i framtida versioner av EF Core som stöder föränderliga nyckelvärden.
Returnerar värden
EF Core stöder tre mekanismer för att returnera värden från lagrade procedurer:
- Utdataparametrar, som visas ovan.
- Resultatkolumner som anges med
HasResultColumnbuildermetoden. - Returvärdet, som är begränsat till att returnera antalet rader som påverkas, anges med hjälp av buildermetoden
HasRowsAffectedReturnValue.
Värden som returneras från lagrade procedurer används ofta för genererade, standardvärden eller beräknade värden, till exempel från en Identity nyckel eller en beräknad kolumn. Följande konfiguration anger till exempel fyra resultatkolumner:
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);
});
Dessa används för att returnera:
- Det genererade nyckelvärdet för
Idegenskapen. - Standardvärdet som genereras av databasen för
FirstRecordedOnegenskapen. - Det beräknade värdet som genereras av databasen för
RetrievedOnegenskapen. - Den automatiskt genererade samtidighetstoken
rowversionförRowVersionegenskapen.
Den här konfigurationen mappar till följande lagrade procedur när du använder 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
Optimistisk konkurrens
Optimistisk samtidighet fungerar på samma sätt med lagrade procedurer som utan. Den lagrade proceduren bör:
- Använd en samtidighetstoken i en
WHERE-sats för att säkerställa att raden endast uppdateras om den har en giltig token. Värdet som används för samtidighetstoken är vanligtvis, men behöver inte vara, det ursprungliga värdet för egenskapen samtidighetstoken. - Returnera antalet rader som påverkas så att EF Core kan jämföra detta med det förväntade antalet rader som påverkas och utlösa ett
DbUpdateConcurrencyExceptionom värdena inte matchar.
Följande lagrade SQL Server-procedur använder till exempel en rowversion automatisk samtidighetstoken:
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
Detta konfigureras i EF Core med hjälp av:
.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();
});
Observera att:
- Det ursprungliga värdet för
RowVersionsamtidighetstoken används. - Den lagrade proceduren använder en
WHEREsats för att säkerställa att raden endast uppdateras om detRowVersionursprungliga värdet matchar. - Det nya genererade värdet för
RowVersioninfogas i en tillfällig tabell. - Antalet rader som påverkas (
@@ROWCOUNT) och det genereradeRowVersionvärdet returneras.
Mappa arvshierarkier till lagrade procedurer
EF Core kräver att lagrade procedurer följer tabelllayouten för typer i en hierarki. Detta innebär att:
- En hierarki som mappas med TPH måste ha en enda infognings-, uppdaterings- och/eller borttagningsprocedur för den enda mappade tabellen. De infognings- och uppdaterings lagrade procedurerna måste ha en parameter för det diskriminerande värdet.
- En hierarki som mappas med TPT måste ha en infognings-, uppdaterings- och/eller borttagningsprocedur för varje typ, inklusive abstrakta typer. EF Core gör flera anrop efter behov för att uppdatera, infoga och ta bort rader i alla tabeller.
- En hierarki som mappas med TPC måste ha en infognings-, uppdaterings- och/eller borttagningsprocedur för varje konkret typ, men inte abstrakta typer.
Anmärkning
Om du är intresserad av att använda en enda lagrad procedur per konkret typ, oavsett arvsmappningsstrategi, så rösta på Support med hjälp av en enda sproc per konkret typ oavsett arvsmappningsstrategi.
Mappa ägda typer till lagrade procedurer
Konfiguration av lagrade procedurer för ägda typer görs i den kapslade byggaren för ägda typer. Till exempel:
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();
});
});
Anmärkning
För närvarande måste lagrade procedurer för att infoga, uppdatera och ta bort endast stöd för ägda typer mappas till separata tabeller. Det innebär att den ägda typen inte kan representeras av kolumner i ägartabellen. Rösta på Lägg till "tabell"-delningsstöd för CUD-sproc-mappning om detta är en begränsning som du vill se borttagen.
Mappa många-till-många-anslutningsentiteter till lagrade procedurer
Konfiguration av lagrade procedurer för många-till-många-kopplingsentiteter kan utföras som en del av många-till-många-konfigurationen. Till exempel:
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();
});
});
});
Nya och förbättrade interceptorer och händelser
EF Core-avlyssningsanordningar möjliggör avlyssning, ändring och/eller undertryckning av EF Core-åtgärder. EF Core innehåller även traditionella .NET-händelser och loggning.
EF7 innehåller följande förbättringar av interceptorer:
- Avlyssning för att skapa och fylla i nya entitetsinstanser (även kallat "materialisering")
- Avlyssning för att ändra LINQ-uttrycksträdet innan en fråga kompileras
- Avlyssning för optimistisk samtidighetshantering (
DbUpdateConcurrencyException) - Avlyssning för anslutningar innan du kontrollerar om anslutningssträngen har angetts
- Avlyssning för när EF Core har bearbetat en resultatuppsättning, men innan den stängs
- Avlyssning för skapandet av en
DbConnectionav EF Core - För avlyssning av
DbCommandefter att den har initierats
Dessutom innehåller EF7 nya traditionella .NET-händelser för:
- När en entitet är på väg att spåras eller ändra tillstånd, men innan den faktiskt spåras eller ändrar tillstånd
- Före och efter EF Core identifierar ändringar i entiteter och egenskaper (även kallat
DetectChangesinterception)
I följande avsnitt visas några exempel på hur du använder dessa nya avlyssningsfunktioner.
Enkla åtgärder vid skapande av entitet
Tips/Råd
Koden som visas här kommer från SimpleMaterializationSample.cs.
Den nya IMaterializationInterceptor stöder interception före och efter att en entitetsinstans har skapats, och före och efter att egenskaperna för den instansen initieras. Interceptorn kan ändra eller ersätta entitetsinstansen vid varje punkt. På så sätt kan du:
- Ange ommappade egenskaper eller anropa metoder som behövs för validering, beräknade värden eller flaggor.
- Använda en fabrik för att skapa instanser.
- Att skapa en annan entitetsinstans än vad EF normalt skulle skapa, till exempel en instans från en cache eller av en proxytyp.
- Injicera tjänster i en entitetsinstans.
Anta till exempel att vi vill hålla reda på den tid då en entitet hämtades från databasen, kanske så att den kan visas för en användare som redigerar data. För att åstadkomma detta definierar vi först ett gränssnitt:
public interface IHasRetrieved
{
DateTime Retrieved { get; set; }
}
Att använda ett gränssnitt är vanligt med interceptorer eftersom det gör att samma interceptor kan fungera med många olika entitetstyper. Till exempel:
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; }
}
Observera att [NotMapped] attributet används för att ange att den här egenskapen endast används när du arbetar med entiteten och inte bör sparas i databasen.
Interceptorn måste sedan implementera lämplig metod från IMaterializationInterceptor och ange den tid som hämtas:
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
En instans av den här interceptorn registreras när du konfigurerar 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");
}
Tips/Råd
Den här interceptorn är tillståndslös, vilket är vanligt, så en enda instans skapas och delas mellan alla DbContext instanser.
Nu, när en Customer frågas från databasen, kommer egenskapen Retrieved att ställas in automatiskt. Till exempel:
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()}'");
}
Genererar utdata:
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
Mata in tjänster i entiteter
Tips/Råd
Koden som visas här kommer från InjectLoggerSample.cs.
EF Core har redan inbyggt stöd för att mata in vissa särskilda tjänster i kontextinstanser. Se till exempel Lat inläsning utan proxyservrar, vilket fungerar genom att ILazyLoader mata in tjänsten.
En IMaterializationInterceptor kan användas för att generalisera detta till valfri tjänst. I följande exempel visas hur du injicerar ILogger i entiteter, så att de kan utföra sin egen loggning.
Anmärkning
När tjänster matas in i entiteter kopplas dessa entitetstyper till de inmatade tjänsterna, som vissa anser vara ett antimönster.
Precis som tidigare används ett gränssnitt för att definiera vad som kan göras.
public interface IHasLogger
{
ILogger? Logger { get; set; }
}
Och entitetstyper som loggar måste implementera det här gränssnittet. Till exempel:
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; }
}
Den här gången måste interceptorn implementera IMaterializationInterceptor.InitializedInstance, som anropas efter att varje entitetsinstans har skapats och dess egenskapsvärden har initierats. Interceptorn hämtar en ILogger från kontexten och initierar IHasLogger.Logger med den:
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;
}
}
Den här gången används en ny instans av interceptorn för varje DbContext instans, eftersom den erhållna ILogger kan ändras per DbContext instans och ILogger cachelagras på interceptorn:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());
När ändringen Customer.PhoneNumber ändras loggas nu den här ändringen i programmets logg. Till exempel:
info: CustomersLogger[1]
Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.
LINQ-uttrycksträdsavlyssning
Tips/Råd
Koden som visas här kommer från QueryInterceptionSample.cs.
EF Core använder .NET LINQ-frågor. Detta innebär vanligtvis att du använder C#-, VB- eller F#-kompilatorn för att skapa ett uttrycksträd som sedan översätts av EF Core till lämplig SQL. Tänk dig till exempel en metod som returnerar en sida med kunder:
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();
}
Tips/Råd
Den här frågan använder EF.Property metoden för att ange den egenskap som ska sorteras efter. På så sätt kan programmet dynamiskt skicka in egenskapsnamnet, vilket tillåter sortering efter valfri egenskap av entitetstyp. Tänk på att sortering efter icke-indexerade kolumner kan vara långsam.
Detta fungerar bra så länge egenskapen som används för sortering alltid returnerar en stabil beställning. Men så är det kanske inte alltid. Linq-frågan ovan genererar till exempel följande på SQLite när du beställer efter 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
Om det finns flera kunder med samma City är sorteringen av den här frågan inte stabil. Detta kan leda till förlorade eller duplicerade resultat när användaren bläddrar genom data.
Ett vanligt sätt att åtgärda det här problemet är att utföra en sekundär sortering efter primärnyckel. Men i stället för att lägga till detta manuellt i varje fråga tillåter EF7 avlyssning av frågeuttrycksträdet där den sekundära ordningen kan läggas till dynamiskt. För att underlätta detta använder vi återigen ett gränssnitt, den här gången för alla entiteter som har en heltalsnyckel:
public interface IHasIntKey
{
int Id { get; }
}
Det här gränssnittet implementeras av entitetstyperna av intresse:
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; }
}
Sedan behöver vi en interceptor som implementerar 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);
}
}
}
Detta ser förmodligen ganska komplicerat- och det är det! Det är vanligtvis inte lätt att arbeta med uttrycksträd. Nu ska vi titta på vad som händer:
I grund och botten kapslar interceptorn in en ExpressionVisitor. Besökaren åsidosätter VisitMethodCall, som anropas när det finns ett anrop till en metod i frågeuttrycksträdet.
Besökaren kontrollerar om detta är ett anrop till den OrderBy metod som vi är intresserade av.
Om det är det kontrollerar besökaren ytterligare om det generiska metodanropet är för en typ som implementerar vårt
IHasIntKeygränssnitt.Nu vet vi att metodanropet är av formatet
OrderBy(e => ...). Vi extraherar lambda-uttrycket från det här anropet och hämtar parametern som används i uttrycket ,edet vill säga .Nu skapar vi en ny MethodCallExpression med hjälp av Expression.Call builder-metoden. I det här fallet är
ThenBy(e => e.Id)metoden som anropas . Vi bygger detta med hjälp av parametern som extraherats ovan och åtkomst till egenskapenIdpå gränssnittetIHasIntKey.Indata i det här anropet är det ursprungliga
OrderBy(e => ...), så slutresultatet är ett uttryck förOrderBy(e => ...).ThenBy(e => e.Id).Det här ändrade uttrycket returneras från besökaren, vilket innebär att LINQ-frågan nu har ändrats korrekt för att inkludera ett
ThenByanrop.EF Core fortsätter och kompilerar det här frågeuttrycket till lämplig SQL för den databas som används.
Den här interceptorn är registrerad på samma sätt som vi gjorde i det första exemplet. Att köra GetPageOfCustomers genererar nu följande 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
Detta skapar nu alltid en stabil beställning, även om det finns flera kunder med samma City.
Puh! Det är mycket kod för att göra en enkel ändring i en fråga. Och ännu värre är att det kanske inte ens fungerar för alla frågor. Det är notoriskt svårt att skriva en expression visitor som identifierar alla frågestrukturer som den borde, och ingen av de som den inte bör. Detta kommer till exempel sannolikt inte att fungera om beställningen görs i en underfråga.
Detta leder oss till en kritisk punkt om interceptorer - fråga dig alltid om det finns ett enklare sätt att göra vad du vill. Interceptorer är kraftfulla, men det är lätt att få saker fel. De är, som talesättet säger, ett enkelt sätt att skjuta dig själv i foten.
Tänk dig till exempel om vi i stället ändrade vår GetPageOfCustomers metod så här:
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();
}
I det här fallet ThenBy läggs helt enkelt till i frågan. Ja, det kan behöva göras separat till varje fråga, men det är enkelt, lätt att förstå och kommer alltid att fungera.
Optimistisk samtidighetsavlyssning
Tips/Råd
Koden som visas här kommer från OptimisticConcurrencyInterceptionSample.cs.
EF Core stöder det optimistiska samtidighetsmönstret genom att kontrollera att antalet rader som faktiskt påverkas av en uppdatering eller borttagning är detsamma som antalet rader som förväntas påverkas. Detta är ofta kopplat till en samtidighetstoken. det vill: ett kolumnvärde som endast matchar det förväntade värdet om raden inte har uppdaterats sedan det förväntade värdet lästes.
EF signalerar ett brott mot optimistisk samtidighet genom att kasta en DbUpdateConcurrencyException. I EF7 har ISaveChangesInterceptor fått nya metoder ThrowingConcurrencyException och ThrowingConcurrencyExceptionAsync som anropas innan DbUpdateConcurrencyException kastas. Dessa avlyssningspunkter tillåter att undantaget ignoreras, eventuellt tillsammans med asynkrona databasändringar för att lösa överträdelsen.
Om två begäranden till exempel försöker ta bort samma entitet på nästan samma gång kan den andra borttagningen misslyckas eftersom raden i databasen inte längre finns. Detta kan vara bra – slutresultatet är att entiteten ändå har tagits bort. Följande interceptor visar hur detta kan göras:
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));
}
Det finns flera saker som är värda att notera om den här interceptorn:
- Både synkrona och asynkrona avlyssningsmetoder implementeras. Detta är viktigt om programmet kan anropa antingen
SaveChangesellerSaveChangesAsync. Men om all programkod är asynkron behöver den baraThrowingConcurrencyExceptionAsyncimplementeras. På samma sätt, om programmet aldrig använder asynkrona databasmetoder, behöver det baraThrowingConcurrencyExceptionimplementeras. Detta gäller vanligtvis för alla interceptorer med synkroniserings- och asynkroniseringsmetoder. (Det kan vara värt att implementera metoden som programmet inte använder för att kasta, ifall någon synkroniserings-/asynkron kod kryper in.) - Interceptorn har åtkomst till EntityEntry objekt för de entiteter som sparas. I det här fallet används detta för att kontrollera om samtidighetsöverträdelsen sker för en borttagningsåtgärd.
- Om programmet använder en relationsdatabasprovider kan objektet ConcurrencyExceptionEventData omvandlas till ett RelationalConcurrencyExceptionEventData objekt. Detta ger ytterligare, relationsspecifik information om databasåtgärden som utförs. I det här fallet skrivs relationskommandotexten ut till konsolen.
- Genom att återvända
InterceptionResult.Suppress()uppmanas EF Core att undertrycka den åtgärd den var på väg att vidta—i det här fallet att kastaDbUpdateConcurrencyException. Den här möjligheten att ändra EF Cores beteende, snarare än att bara observera vad EF Core gör, är en av de mest kraftfulla funktionerna i interceptorer.
Fördröjd initiering av en anslutningssträng
Tips/Råd
Koden som visas här kommer från LazyConnectionStringSample.cs.
Anslutningssträngar är ofta statiska tillgångar som läses från en konfigurationsfil. Dessa kan enkelt skickas till UseSqlServer eller liknande när du konfigurerar en DbContext. Ibland kan dock anslutningssträngen ändras för varje kontextinstans. Till exempel kan varje klientorganisation i ett system med flera klientorganisationer ha en annan anslutningssträng.
EF7 gör det enklare att hantera dynamiska anslutningar och anslutningssträngar genom förbättringar av IDbConnectionInterceptor. Detta börjar med möjligheten att konfigurera DbContext utan någon anslutningssträng. Till exempel:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer());
En av IDbConnectionInterceptor metoderna kan sedan implementeras för att konfigurera anslutningen innan den används.
ConnectionOpeningAsync är ett bra val eftersom det kan utföra en asynkron åtgärd för att hämta anslutningssträngen, hitta en åtkomsttoken och så vidare. Anta till exempel att en tjänst är avgränsad till den nuvarande begäran och förstår den nuvarande klientorganisationen.
services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();
Varning
Det kan gå mycket långsamt att utföra en asynkron sökning efter en anslutningssträng, åtkomsttoken eller liknande varje gång den behövs. Överväg att cachelagra dessa saker och endast uppdatera den cachelagrade strängen eller token med jämna mellanrum. Åtkomsttoken kan till exempel ofta användas under en längre tid innan de behöver uppdateras.
Detta kan matas in i varje DbContext instans med konstruktorinmatning:
public class CustomerContext : DbContext
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public CustomerContext(
DbContextOptions<CustomerContext> options,
ITenantConnectionStringFactory connectionStringFactory)
: base(options)
{
_connectionStringFactory = connectionStringFactory;
}
// ...
}
Den här tjänsten används sedan när du skapar interceptor-implementeringen för kontexten:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(
new ConnectionStringInitializationInterceptor(_connectionStringFactory));
Slutligen använder interceptorn den här tjänsten för att hämta anslutningssträngen asynkront och ange den första gången som anslutningen används:
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;
}
}
Anmärkning
Anslutningssträngen hämtas bara första gången som en anslutning används. Därefter används anslutningssträngen som lagras på den DbConnection utan att leta upp en ny anslutningssträng.
Tips/Råd
Den här interceptorn åsidosätter den icke-asynkrona ConnectionOpening metod som ska utlösas eftersom tjänsten för att hämta anslutningssträngen måste anropas från en asynkron kodsökväg.
Loggning av SQL Server-frågestatistik
Tips/Råd
Koden som visas här kommer från QueryStatisticsLoggerSample.cs.
Slutligen ska vi skapa två interceptorer som fungerar tillsammans för att skicka SQL Server-frågestatistik till programloggen. För att generera statistiken behöver vi en IDbCommandInterceptor för att göra två saker.
Först kommer interceptorn att prefixa kommandon med SET STATISTICS IO ON, vilket instruerar SQL Server att skicka statistik till klienten efter att ett resultatset har förbrukats.
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);
}
För det andra kommer interceptorn att implementera den nya metoden DataReaderClosingAsync, som anropas efter att DbDataReader har förbrukat resultaten, men innan det har stängts. När SQL Server skickar statistik placerar den dem i ett andra resultat på läsaren, så just nu läser interceptorn det resultatet genom att anropa NextResultAsync som fyller statistik på anslutningen.
public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
DbCommand command,
DataReaderClosingEventData eventData,
InterceptionResult result)
{
await eventData.DataReader.NextResultAsync();
return result;
}
Den andra interceptorn behövs för att hämta statistiken från anslutningen och skriva ut dem till programmets logger. För detta använder vi en IDbConnectionInterceptor, som implementerar den nya ConnectionCreated metoden.
ConnectionCreated anropas omedelbart efter att EF Core har skapat en anslutning och kan därför användas för att utföra ytterligare konfiguration av anslutningen. I det här fallet hämtar interceptorn en ILogger och ansluter sedan till händelsen SqlConnection.InfoMessage för att logga meddelandena.
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;
}
Viktigt!
Metoderna ConnectionCreating och ConnectionCreated anropas bara när EF Core skapar en DbConnection. De anropas inte om programmet skapar DbConnection och skickar det till EF Core.
När du kör kod som använder dessa interceptorer visas SQL Server-frågestatistik i loggen:
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.
Frågeförbättringar
EF7 innehåller många förbättringar i översättningen av LINQ-frågor.
GroupBy som slutlig operator
Tips/Råd
Koden som visas här kommer från GroupByFinalOperatorSample.cs.
EF7 stöder användning GroupBy som den sista operatorn i en fråga. Till exempel den här LINQ-frågan:
var query = context.Books.GroupBy(s => s.Price);
Översätts till följande SQL när du använder SQL Server:
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]
Anmärkning
Den här typen av GroupBy översätts inte direkt till SQL, så EF Core grupperar de returnerade resultaten. Detta resulterar dock inte i att ytterligare data överförs från servern.
GroupJoin som slutlig operator
Tips/Råd
Koden som visas här kommer från GroupJoinFinalOperatorSample.cs.
EF7 stöder användning GroupJoin som den sista operatorn i en fråga. Till exempel den här LINQ-frågan:
var query = context.Customers.GroupJoin(
context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });
Översätts till följande SQL när du använder 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]
GroupBy-entitetstyp
Tips/Råd
Koden som visas här kommer från GroupByEntityTypeSample.cs.
EF7 stöder gruppering efter en entitetstyp. Till exempel den här LINQ-frågan:
var query = context.Books
.GroupBy(s => s.Author)
.Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });
Översätts till följande SQL när du använder 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]
Tänk på att gruppering efter en unik egenskap, till exempel primärnyckeln, alltid är effektivare än att gruppera efter en entitetstyp. Gruppering efter entitetstyper kan dock användas för både nyckellösa och nyckellösa entitetstyper.
Gruppering efter en entitetstyp med en primärnyckel resulterar alltid i en grupp per entitetsinstans, eftersom varje entitet måste ha ett unikt nyckelvärde. Ibland är det värt att byta källa för frågan så att gruppering inte krävs. Följande fråga returnerar till exempel samma resultat som föregående fråga:
var query = context.Authors
.Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });
Den här frågan översätts till följande SQL när du använder 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]
Underfrågor refererar inte till ogrupperade kolumner från yttre fråga
Tips/Råd
Koden som visas här kommer från UngroupedColumnsQuerySample.cs.
I EF Core 6.0 refererar ett GROUP BY villkor till kolumner i det yttre frågeuttrycket, vilket misslyckas med vissa databaser och är ineffektivt i andra. Tänk dig följande fråga:
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)
};
I EF Core 6.0 på SQL Server översattes detta till:
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])
På EF7 är översättningen:
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]
Skrivskyddade samlingar kan användas för Contains
Tips/Råd
Koden som visas här kommer från ReadOnlySetQuerySample.cs.
EF7 stöder användning Contains när objekten som ska sökas efter finns i en IReadOnlySet eller IReadOnlyCollection, eller IReadOnlyList. Till exempel den här LINQ-frågan:
IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));
Översätts till följande SQL när du använder 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))
Översättningar för aggregerade funktioner
EF7 ger bättre utökningsbarhet för leverantörer för att översätta aggregerade funktioner. Detta och annat arbete på detta område har resulterat i flera nya översättningar mellan leverantörer, bland annat:
-
Översättning av
String.JoinochString.Concat - Översättning av rumsliga aggregeringsfunktioner
- Översättning av statistikaggregatfunktioner
Anmärkning
Aggregerade funktioner som agerar på IEnumerable argument översätts vanligtvis bara i GroupBy frågor. Rösta för Stöd spatiala typer i JSON-kolumner om du vill få denna begränsning borttagen.
Strängaggregatfunktioner
Tips/Råd
Koden som visas här kommer från StringAggregateFunctionsSample.cs.
Frågor som använder Join och Concat översätts nu när det är lämpligt. Till exempel:
var query = context.Posts
.GroupBy(post => post.Author)
.Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });
Den här frågan översätts till följande när du använder 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]
I kombination med andra strängfunktioner möjliggör dessa översättningar viss komplex strängmanipulering på servern. Till exempel:
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) + "' "))
});
Den här frågan översätts till följande när du använder 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]
Rumsliga aggregeringsfunktioner
Tips/Råd
Koden som visas här kommer från SpatialAggregateFunctionsSample.cs.
Det är nu möjligt för databasleverantörer som stöder NetTopologySuite att översätta följande rumsliga aggregeringsfunktioner:
- GeometryCombiner.Combine()
- UnaryUnionOp.Union()
- ConvexHull.Create()
- EnvelopeCombiner.CombineAsGeometry()
Tips/Råd
Dessa översättningar har implementerats av teamet för SQL Server och SQLite. För andra leverantörer kontaktar du providerunderhållaren för att lägga till support om den har implementerats för den leverantören.
Till exempel:
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)) });
Den här frågan översätts till följande SQL när du använder 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]
Statistiska aggregeringsfunktioner
Tips/Råd
Koden som visas här kommer från StatisticalAggregateFunctionsSample.cs.
SQL Server-översättningar har implementerats för följande statistiska funktioner:
Tips/Råd
Dessa översättningar har implementerats av teamet för SQL Server. För andra leverantörer kontaktar du providerunderhållaren för att lägga till support om den har implementerats för den leverantören.
Till exempel:
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))
});
Den här frågan översätts till följande SQL när du använder 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]
Översättning av string.IndexOf
Tips/Råd
Koden som visas här kommer från MiscellaneousTranslationsSample.cs.
Nu översätter EF7 String.IndexOf i LINQ-frågor. Till exempel:
var query = context.Posts
.Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
.Where(post => post.IndexOfEntity > 0);
Den här frågan översätts till följande SQL när du använder 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
Översättning av GetType för entitetstyper
Tips/Råd
Koden som visas här kommer från MiscellaneousTranslationsSample.cs.
Nu översätter EF7 Object.GetType() i LINQ-frågor. Till exempel:
var query = context.Posts.Where(post => post.GetType() == typeof(Post));
Den här frågan översätts till följande SQL när du använder SQL Server med TPH-arv:
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'
Observera att den här frågan endast returnerar Post instanser som faktiskt är av typen Post, och inte sådana av några härledda typer. Detta skiljer sig från en fråga som använder is eller OfType, som också returnerar instanser av alla härledda typer. Tänk till exempel på frågan:
var query = context.Posts.OfType<Post>();
Vilket översätts till olika SQL:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
Och returnerar både Post och FeaturedPost entiteter.
Stöd för AT TIME ZONE
Tips/Råd
Koden som visas här kommer från MiscellaneousTranslationsSample.cs.
EF7 introducerar nya AtTimeZone funktioner för DateTime och DateTimeOffset. Dessa funktioner översätts till AT TIME ZONE satser i den genererade SQL-filen. Till exempel:
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"),
});
Den här frågan översätts till följande SQL när du använder 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]
Tips/Råd
Dessa översättningar har implementerats av teamet för SQL Server. För andra leverantörer kontaktar du providerunderhållaren för att lägga till support om den har implementerats för den leverantören.
Filtrerad Inkludera i dolda navigeringer
Tips/Råd
Koden som visas här kommer från MiscellaneousTranslationsSample.cs.
Include-metoderna kan nu användas med EF.Property. Detta möjliggör filtrering och beställning även för privata navigeringsegenskaper eller privata navigeringar som representeras av fält. Till exempel:
var query = context.Blogs.Include(
blog => EF.Property<ICollection<Post>>(blog, "Posts")
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Detta motsvarar:
var query = context.Blogs.Include(
blog => Posts
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Men kräver Blog.Posts inte att vara offentligt tillgänglig.
När du använder SQL Server översätts båda frågorna ovan till:
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]
Cosmos-översättning för Regex.IsMatch
Tips/Råd
Koden som visas här kommer från CosmosQueriesSample.cs.
EF7 stöder användning i Regex.IsMatch LINQ-frågor mot Azure Cosmos DB. Till exempel:
var containsInnerT = await context.Triangles
.Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
.ToListAsync();
Översätts till följande SQL:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))
Förbättringar av DBContext API och beteende
EF7 innehåller en mängd olika små förbättringar för DbContext och relaterade klasser.
Tips/Råd
Koden för exempel i det här avsnittet kommer från DbContextApiSample.cs.
Suppressor för oinitierade DbSet-egenskaper
Offentliga, inställbara DbSet egenskaper på en DbContext initieras automatiskt av EF Core när den DbContext skapas. Tänk till exempel på följande DbContext definition:
public class SomeDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}
Blogs-egenskapen kommer att anges till en DbSet<Blog>-instans som en del av att konstruera instansen DbContext. Detta gör att kontexten kan användas för frågor utan några ytterligare steg.
Efter introduktionen av nullbara referenstyper för C# varnar kompilatorn nu för att den icke-nullbara egenskapen Blogs inte har initierats:
[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Det här är en falsk varning. egenskapen är inställd på ett värde som inte är null av EF Core. Om du deklarerar egenskapen som null kommer varningen också att försvinna, men det är inte en bra idé eftersom egenskapen konceptuellt sett inte är nullbar och aldrig kommer att vara null.
EF7 innehåller en DiagnosticSuppressor för DbSet egenskaper på en DbContext som hindrar kompilatorn från att generera den här varningen.
Tips/Råd
Det här mönstret har sitt ursprung i de dagar då C#-autoegenskaperna var mycket begränsade. Med modern C#kan du överväga att göra autoegenskaperna skrivskyddade och sedan initiera dem explicit i DbContext konstruktorn eller hämta den cachelagrade DbSet instansen från kontexten när det behövs. Till exempel public DbSet<Blog> Blogs => Set<Blog>().
Skilja annullering från fel i loggar
Ibland avbryter ett program uttryckligen en fråga eller en annan databasåtgärd. Detta görs vanligtvis med hjälp av en CancellationToken skickad till metoden som utför åtgärden.
I EF Core 6 är de händelser som loggas när en åtgärd avbryts samma som de som loggas när åtgärden misslyckas av någon annan anledning. EF7 introducerar nya logghändelser specifikt för avbrutna databasåtgärder. Dessa nya händelser loggas som standard på nivån Debug . I följande tabell visas relevanta händelser och deras standardloggnivåer:
| Evenemang | Beskrivning | Standardloggnivå |
|---|---|---|
| CoreEventId.QueryIterationFailed | Ett fel uppstod vid bearbetning av resultatet av en fråga. | LogLevel.Error |
| CoreEventId.SaveChangesFailed | Ett fel uppstod vid försök att spara ändringar i databasen. | LogLevel.Error |
| RelationalEventId.CommandError | Ett fel uppstod när ett databaskommando kördes. | LogLevel.Error |
| CoreEventId.QueryCanceled | En sökfråga avbröts. | LogLevel.Debug |
| CoreEventId.SaveChangesCanceled | Databaskommandot avbröts när ändringar skulle sparas. | LogLevel.Debug |
| RelationalEventId.CommandCanceled | Körningen av en DbCommand har avbrutits. |
LogLevel.Debug |
Anmärkning
Annullering identifieras genom att titta på undantaget i stället för att kontrollera annulleringstoken. Det innebär att annulleringar som inte utlöses via annulleringstoken fortfarande identifieras och loggas på det här sättet.
Nya överbelastningar för metoder IProperty och INavigationEntityEntry
Kod som arbetar med EF-modellen har ofta en IProperty eller INavigation representerar egenskaps- eller navigeringsmetadata. En EntityEntry används sedan för att hämta egenskapen/navigeringsvärdet eller ta reda på dess tillstånd. Innan EF7 krävde detta dock att namnet på egenskapen eller navigeringen skulle skickas till metoderna i EntityEntry, som sedan skulle slå upp IProperty eller INavigationigen. I EF7 kan IProperty eller INavigation i stället skickas direkt, vilket undviker ytterligare sökning.
Tänk dig till exempel en metod för att hitta alla syskon för en viss entitet:
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));
}
Den här metoden hittar den överordnade för en viss entitet och skickar sedan inversen INavigation till den överordnade postens Collection-metod. Dessa metadata används sedan för att returnera alla syskon till den specificerade föräldern. Här är ett exempel på dess användning:
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}'");
}
Och utdata:
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'
EntityEntry för entitetstyper av delad typ
EF Core kan använda samma CLR-typ för flera olika entitetstyper. Dessa kallas "entitetstyper av delad typ" och används ofta för att mappa en ordlistetyp med nyckel/värde-par som används för egenskaperna för entitetstypen. En entitetstyp BuildMetadata kan till exempel definieras utan att definiera en dedikerad CLR-typ:
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");
});
Observera att entitetstypen av delad typ måste namnges – i det här fallet är BuildMetadatanamnet . Dessa entitetstyper används sedan med hjälp av en DbSet för entitetstypen som hämtas med hjälp av namnet. Till exempel:
public DbSet<Dictionary<string, object>> BuildMetadata
=> Set<Dictionary<string, object>>("BuildMetadata");
Detta DbSet kan användas för att spåra entitetsinstanser:
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" }
});
Och kör frågor:
var builds = await context.BuildMetadata
.Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
.OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
.ToListAsync();
I EF7 finns det nu också en Entry metod DbSet som kan användas för att hämta tillståndet för en instans, även om den ännu inte spåras. Till exempel:
var state = context.BuildMetadata.Entry(build).State;
ContextInitialized loggas nu som Debug
I EF7 loggas ContextInitialized-händelsen på Debug-nivån. Till exempel:
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
I tidigare versioner loggades den på nivån Information . Till exempel:
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
Om du vill kan loggnivån ändras tillbaka till Information:
optionsBuilder.ConfigureWarnings(
builder =>
{
builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
});
IEntityEntryGraphIterator är offentligt användbar
I EF7 IEntityEntryGraphIterator kan tjänsten användas av program. Det här är den tjänst som används internt när du identifierar ett diagram över entiteter som ska spåras och även av TrackGraph. Här är ett exempel som itererar över alla entiteter som kan nås från någon startentitet:
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();
Meddelanden:
- Iteratorn slutar att passera från en viss nod när återanropsdelegaten returnerar
false. Det här exemplet håller reda på besökta entiteter och returnerarfalsenär entiteten redan har besökts. På så sätt förhindras oändliga loopar till följd av cykler i diagrammet. - Med
EntityEntryGraphNode<TState>objektet kan tillståndet skickas runt utan att det kan hämtas till ombudet. - För varje nod som besöktes förutom den första, skickas den nod som den identifierades från och navigeringen som den identifierades via till återanropet.
Förbättringar av modellbyggande
EF7 innehåller en mängd olika små förbättringar i modellbygget.
Tips/Råd
Koden för exempel i det här avsnittet kommer från ModelBuildingSample.cs.
Index kan vara stigande eller fallande
Som standard skapar EF Core stigande index. EF7 stöder också skapandet av fallande index. Till exempel:
modelBuilder
.Entity<Post>()
.HasIndex(post => post.Title)
.IsDescending();
Eller så använder du mappningsattributet Index :
[Index(nameof(Title), AllDescending = true)]
public class Post
{
public int Id { get; set; }
[MaxLength(64)]
public string? Title { get; set; }
}
Detta är sällan användbart för index över en enda kolumn, eftersom databasen kan använda samma index för att sortera i båda riktningarna. Detta gäller dock inte för sammansatta index över flera kolumner där ordningen på varje kolumn kan vara viktig. EF Core stöder detta genom att tillåta att flera kolumner har olika ordningsföljder definierade för varje kolumn. Till exempel:
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner })
.IsDescending(false, true);
Eller så använder du ett mappningsattribut:
[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();
}
Detta resulterar i följande SQL när du använder SQL Server:
CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);
Slutligen kan flera index skapas över samma ordnade uppsättning kolumner genom att ge indexnamnen. Till exempel:
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);
Eller så använder du mappningsattribut:
[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();
}
Detta genererar följande SQL på 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);
Mappningsattribut för sammansatta nycklar
EF7 introducerar ett nytt mappningsattribut (även kallat "dataanteckning") för att ange primärnyckelegenskapen eller egenskaperna för alla entitetstyper. System.ComponentModel.DataAnnotations.KeyAttribute Till skillnad från PrimaryKeyAttributeplaceras på entitetstypklassen i stället för på nyckelegenskapen. Till exempel:
[PrimaryKey(nameof(PostKey))]
public class Post
{
public int PostKey { get; set; }
}
Detta gör det till en naturlig passform för att definiera sammansatta nycklar:
[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
public int PostId { get; set; }
public int CommentId { get; set; }
public string CommentText { get; set; } = null!;
}
Att definiera indexet för klassen innebär också att det kan användas för att ange privata egenskaper eller fält som nycklar, även om dessa vanligtvis ignoreras när du skapar EF-modellen. Till exempel:
[PrimaryKey(nameof(_id))]
public class Tag
{
private readonly int _id;
}
DeleteBehavior mappningsattribut
EF7 introducerar ett mappningsattribut (även kallat "dataanteckning") för att ange DeleteBehavior för en relation. Till exempel skapas nödvändiga relationer med DeleteBehavior.Cascade som standard. Detta kan ändras till DeleteBehavior.NoAction som standard med hjälp av DeleteBehaviorAttribute:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
[DeleteBehavior(DeleteBehavior.NoAction)]
public Blog Blog { get; set; } = null!;
}
Detta inaktiverar kaskadborttagningar för relationen Blog-Posts.
Egenskaper som mappats till olika kolumnnamn
Vissa mappningsmönster resulterar i att samma CLR-egenskap mappas till en kolumn i var och en av flera olika tabeller. MED EF7 kan dessa kolumner ha olika namn. Tänk dig till exempel en enkel arvshierarki:
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; }
}
Med strategin för TPT-arvsmappning mappas dessa typer till tre tabeller. Den primära nyckelkolumnen i varje tabell kan dock ha ett annat namn. Till exempel:
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
);
MED EF7 kan den här mappningen konfigureras med hjälp av en kapslad tabellbyggare:
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"));
Med TPC-arvsmappningen kan egenskapen Breed också mappas till olika kolumnnamn i olika tabeller. Tänk till exempel på följande TPC-tabeller:
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])
);
EF7 stöder den här tabellmappningen:
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");
});
Enkelriktade många-till-många-relationer
EF7 stöder många-till-många-relationer där den ena sidan eller den andra inte har någon navigeringsegenskap. Tänk till exempel på Post och Tag typer:
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!;
}
Observera att Post typen har en navigeringsegenskap för en lista med taggar, men Tag typen har ingen navigeringsegenskap för inlägg. I EF7 kan detta fortfarande konfigureras som en många-till-många-relation, vilket gör att samma Tag objekt kan användas för många olika inlägg. Till exempel:
modelBuilder
.Entity<Post>()
.HasMany(post => post.Tags)
.WithMany();
Detta resulterar i mappning till lämplig kopplingstabell:
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
);
Och relationen kan användas som många-till-många på vanligt sätt. Du kan till exempel infoga vissa inlägg som delar olika taggar från en gemensam uppsättning:
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();
Entitetsdelning
Entitetsdelning mappar en enskild entitetstyp till flera tabeller. Tänk dig till exempel en databas med tre tabeller som innehåller kunddata:
- En
Customerstabell för kundinformation - En
PhoneNumberstabell för kundens telefonnummer - En
Addressestabell för kundens adress
Här är definitioner för dessa tabeller i 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
);
Var och en av dessa tabeller mappas vanligtvis till sin egen entitetstyp, med relationer mellan typerna. Men om alla tre tabellerna alltid används tillsammans kan det vara enklare att mappa dem alla till en enda entitetstyp. Till exempel:
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; }
}
Detta uppnås i EF7 genom att anropa SplitToTable för varje delning i entitetstypen. Följande kod uppdelar till exempel entitetstypen Customer i tabellerna Customers, PhoneNumbers, och Addresses som visas ovan.
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);
});
});
Observera också att du vid behov kan ange olika kolumnnamn för primärnyckeln för var och en av tabellerna.
SQL Server UTF-8-strängar
SQL Server Unicode-strängar som representeras av datatypernanchar och nvarchar lagras som UTF-16. Dessutom används datatypernachar och varchar för att lagra icke-Unicode-strängar med stöd för olika teckenuppsättningar.
Från och med SQL Server 2019 kan datatyperna char och varchar användas för att i stället lagra Unicode-strängar med UTF-8-kodning . Uppnås genom att ange en av UTF-8-sorteringarna. Följande kod konfigurerar till exempel en SQL Server UTF-8-sträng med variabellängd för CommentText kolumnen:
modelBuilder
.Entity<Comment>()
.Property(comment => comment.CommentText)
.HasColumnType("varchar(max)")
.UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");
Den här konfigurationen genererar följande SQL Server-kolumndefinition:
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])
);
Temporala tabeller stöder ägda entiteter
EF Core SQL Server-mappning av temporala tabeller har förbättrats i EF7 för att stödja tabelldelning. I synnerhet använder standardmappningen för ägda enskilda entiteter tabelldelning.
Överväg till exempel en ägarentitetstyp Employee och dess ägda entitetstyp EmployeeInfo:
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; }
}
Om dessa typer mappas till samma tabell kan tabellen i EF7 göras till en temporal tabell:
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");
}));
Anmärkning
Att göra den här konfigurationen enklare spåras av problem nr 29303. Rösta på den här frågan om det är något du vill se implementerat.
Förbättrad värdegenerering
EF7 innehåller två viktiga förbättringar av den automatiska genereringen av värden för nyckelegenskaper.
Tips/Råd
Koden för exempel i det här avsnittet kommer från ValueGenerationSample.cs.
Värdegenerering för DDD-skyddade typer
I domändriven design (DDD) kan "skyddade nycklar" förbättra typsäkerheten för nyckelegenskaper. Detta uppnås genom att omsluta nyckeltypen i en annan typ som är specifik för användningen av nyckeln. Följande kod definierar till exempel en ProductId typ för produktnycklar och en CategoryId typ för kategorinycklar.
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; }
}
Dessa används sedan i Product och Category entitetstyper:
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();
}
Detta gör det omöjligt att oavsiktligt tilldela ID:t för en kategori till en produkt, eller tvärtom.
Varning
Precis som med många DDD-begrepp sker denna förbättrade typsäkerhet på bekostnad av ytterligare kodkomplexitet. Det är värt att överväga om till exempel tilldelning av ett produkt-ID till en kategori är något som någonsin kommer att hända. Att hålla det enkelt kan generellt sett vara mer fördelaktigt för kodbasen.
De skyddade nyckeltyperna som visas här omsluter int båda nyckelvärdena, vilket innebär att heltalsvärden används i de mappade databastabellerna. Detta uppnås genom att definiera värdekonverterare för typerna:
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))
{
}
}
Anmärkning
Koden här använder struct typer. Det innebär att de har lämplig semantik av värdetyp för användning som nycklar. Om class typer används i stället måste de antingen åsidosätta likhetssemantik eller även ange en värdejämförare.
I EF7 kan nyckeltyper baserade på värdekonverterare använda automatiskt genererade nyckelvärden så länge den underliggande typen stöder detta. Detta konfigureras på vanligt sätt med hjälp av ValueGeneratedOnAdd:
modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();
Som standard resulterar detta i IDENTITY kolumner när de används med 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);
Som används på normalt sätt för att generera nyckelvärden vid infogning av entiteter:
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;
Sekvensbaserad nyckelgenerering för SQL Server
EF Core stöder generering av nyckelvärden med hjälp av SQL Server-kolumner IDENTITYeller ett Hi-Lo-mönster baserat på nyckelblock som genereras av en databassekvens. EF7 introducerar stöd för en databassekvens som är kopplad till nyckelns kolumnstandardvillkor. I sin enklaste form kräver detta bara att EF Core uppmanas att använda en sekvens för nyckelegenskapen:
modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();
Detta resulterar i att en sekvens definieras i databasen:
CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;
Som sedan används i standardvillkoret för nyckelkolumnen:
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);
Anmärkning
Den här typen av nyckelgenerering används som standard för genererade nycklar i entitetstyphierarkier med hjälp av TPC-mappningsstrategin.
Om du vill kan sekvensen få ett annat namn och schema. Till exempel:
modelBuilder
.Entity<Product>()
.Property(product => product.Id)
.UseSequence("ProductsSequence", "northwind");
Ytterligare konfiguration av sekvensen skapas genom att konfigurera den explicit i modellen. Till exempel:
modelBuilder
.HasSequence<int>("ProductsSequence", "northwind")
.StartsAt(1000)
.IncrementsBy(2);
Förbättringar av migreringsverktyg
EF7 innehåller två viktiga förbättringar när du använder kommandoradsverktygen för EF Core Migrations.
UseSqlServer osv. acceptera null
Det är mycket vanligt att läsa en anslutningssträng från en konfigurationsfil och sedan skicka anslutningssträngen till UseSqlServer, UseSqliteeller motsvarande metod för en annan provider. Till exempel:
services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));
Det är också vanligt att skicka en anslutningssträng vid tillämpning av migreringar. Till exempel:
dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
Eller när du använder ett migreringspaket.
./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
I det här fallet, även om anslutningssträngen som lästs från konfigurationen inte används, försöker programmets startkod fortfarande läsa den från konfigurationen och skicka den till UseSqlServer. Om konfigurationen inte är tillgänglig resulterar det i att null skickas till UseSqlServer. I EF7 tillåts detta så länge anslutningssträngen slutligen anges senare, till exempel genom att skicka --connection till kommandoradsverktyget.
Anmärkning
Den här ändringen har gjorts för UseSqlServer och UseSqlite. För andra leverantörer kontaktar du leverantörsunderhållaren för att göra en motsvarande ändring om den ännu inte har gjorts för leverantören.
Identifiera när verktyg körs
EF Core kör programkod när dotnet-efeller PowerShell-kommandon används. Ibland kan det vara nödvändigt att identifiera den här situationen för att förhindra att olämplig kod körs vid designtillfället. Kod som till exempel automatiskt tillämpar migreringar vid start bör förmodligen inte göra detta vid designtillfället. I EF7 kan detta identifieras med hjälp av EF.IsDesignTime flaggan:
if (!EF.IsDesignTime)
{
await context.Database.MigrateAsync();
}
EF Core anger IsDesignTime till true när programkoden körs för verktygens räkning.
Prestandaförbättringar för proxyservrar
EF Core stöder dynamiskt genererade proxyservrar för lazy-loading och ändringsspårning. EF7 innehåller två prestandaförbättringar när du använder dessa proxyservrar:
- Proxytyperna skapas nu fördröjt. Det innebär att den första modellbyggtiden när du använder proxyservrar kan vara mycket snabbare med EF7 än med EF Core 6.0.
- Proxyservrar kan nu användas med kompilerade modeller.
Här följer några prestandaresultat för en modell med 449 entitetstyper, 6 390 egenskaper och 720 relationer.
| Scenarium | Metod | Medelvärde | Fel | StdDev |
|---|---|---|---|---|
| EF Core 6.0 utan proxyservrar | TidTillFörstaFråga | 1,085 s | 0,0083 s | 0,0167 s |
| EF Core 6.0 med proxyservrar för ändringsspårning | TidTillFörstaFråga | 13,01 s | 0.2040 s | 0,4110 s |
| EF Core 7.0 utan proxyservrar | TidTillFörstaFråga | 1,442 s | 0,0134 s | 0,0272 s |
| EF Core 7.0 med proxyservrar för ändringsspårning | TidTillFörstaFråga | 1,446 s | 0,0160 s | 0,0323 s |
| EF Core 7.0 med proxyservrar för ändringsspårning och kompilerad modell | TidTillFörstaFråga | 0,162 s | 0,0062 s | 0,0125 s |
I det här fallet kan därför en modell med proxyservrar för ändringsspårning vara redo att köra den första frågan 80 gånger snabbare i EF7 än vad som var möjligt med EF Core 6.0.
Förstklassig Windows Forms-databindning
Windows Forms-teamet har gjort några bra förbättringar av Visual Studio Designer-upplevelsen. Detta inkluderar nya funktioner för databindning som integreras väl med EF Core.
I korthet ger den nya upplevelsen Visual Studio U.I. för att skapa en ObjectDataSource:
Detta kan sedan bindas till en EF Core DbSet med lite enkel kod:
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;
}
}
Se Komma igång med Windows Forms för en fullständig genomgång och nedladdningsbara WinForms-exempelprogram.