Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
EF Core 7.0 (EF7) è stato rilasciato a novembre 2022.
Suggerimento
È possibile eseguire ed eseguire il debug negli esempi scaricando il codice di esempio da GitHub. Ogni sezione è collegata al codice sorgente specifico di tale sezione.
EF7 è destinato a .NET 6 e quindi può essere usato con .NET 6 (LTS) o .NET 7.
Modello di esempio
Molti degli esempi seguenti usano un modello semplice con blog, post, tag e autori:
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();
}
Alcuni esempi usano anche tipi di aggregazione, mappati in modi diversi in esempi diversi. Esiste un tipo di aggregazione per i contatti:
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; }
}
E un secondo tipo di aggregazione per i metadati post:
public class PostMetadata
{
public PostMetadata(int views)
{
Views = views;
}
public int Views { get; set; }
public List<SearchTerm> TopSearches { get; } = new();
public List<Visits> TopGeographies { get; } = new();
public List<PostUpdate> Updates { get; } = new();
}
public class SearchTerm
{
public SearchTerm(string term, int count)
{
Term = term;
Count = count;
}
public string Term { get; private set; }
public int Count { get; private set; }
}
public class Visits
{
public Visits(double latitude, double longitude, int count)
{
Latitude = latitude;
Longitude = longitude;
Count = count;
}
public double Latitude { get; private set; }
public double Longitude { get; private set; }
public int Count { get; private set; }
public List<string>? Browsers { get; set; }
}
public class PostUpdate
{
public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
{
PostedFrom = postedFrom;
UpdatedOn = updatedOn;
}
public IPAddress PostedFrom { get; private set; }
public string? UpdatedBy { get; init; }
public DateTime UpdatedOn { get; private set; }
public List<Commit> Commits { get; } = new();
}
public class Commit
{
public Commit(DateTime committedOn, string comment)
{
CommittedOn = committedOn;
Comment = comment;
}
public DateTime CommittedOn { get; private set; }
public string Comment { get; set; }
}
Suggerimento
Il modello di esempio è disponibile in BlogsContext.cs.
Colonne JSON
La maggior parte dei database relazionali supporta colonne che contengono documenti JSON. Il codice JSON in queste colonne può essere esplorato tramite query. Ciò consente, ad esempio, di filtrare e ordinare in base agli elementi dei documenti, nonché la proiezione di elementi dai documenti nei risultati. Le colonne JSON consentono ai database relazionali di assumere alcune delle caratteristiche dei database di documenti, creando un ibrido utile tra i due.
EF7 contiene il supporto indipendente dal provider per le colonne JSON, con un'implementazione per SQL Server. Questo supporto consente il mapping delle aggregazioni create dai tipi .NET ai documenti JSON. Le normali query LINQ possono essere usate nelle aggregazioni e queste verranno convertite nei costrutti di query appropriati necessari per eseguire il drill-into del codice JSON. EF7 supporta anche l'aggiornamento e il salvataggio delle modifiche ai documenti JSON.
Nota
Il supporto di SQLite per JSON è pianificato per il post EF7. I provider PostgreSQL e Pomelo MySQL contengono già il supporto per le colonne JSON. Microsoft lavorerà con gli autori di questi provider per allineare il supporto JSON in tutti i provider.
Mappatura delle colonne JSON
In EF Core i tipi di aggregazione vengono definiti usando OwnsOne e OwnsMany. Si consideri ad esempio il tipo di aggregazione del modello di esempio usato per archiviare le informazioni di contatto:
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; }
}
Può quindi essere usato in un tipo di entità "proprietario", ad esempio per archiviare i dettagli di contatto di un autore:
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ContactDetails Contact { get; set; }
}
Il tipo di aggregazione è configurato in OnModelCreating usando OwnsOne:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
Suggerimento
Il codice illustrato di seguito proviene da JsonColumnsSample.cs.
Per impostazione predefinita, i provider di database relazionali eseguono il mapping di tipi di aggregazione come questo sulla stessa tabella del tipo di entità proprietario. Ovvero, ogni proprietà delle ContactDetails classi e Address viene mappata a una colonna nella Authors tabella.
Alcuni autori salvati con i dettagli di contatto avranno un aspetto simile al seguente:
Autori
| ID | Nome | Contatto_Indirizzo_Via | Contatto_Indirizzo_Città | Contatto_Indirizzo_CodicePostale | Paese_Indirizzo_Contatto | Telefono_Contatto |
|---|---|---|---|---|---|---|
| 1 | Maddy Montaquila | 1 Principale St | Camberwick Green | CW1 5ZH (codice postale del Regno Unito) | REGNO UNITO | 01632 12345 |
| 2 | Jeremy Likness | 2 Main St | Chigley | CW1 5ZH | REGNO UNITO | 01632 12346 |
| 3 | Daniel Roth | 3 Principale St | Camberwick Green | CW1 5ZH | REGNO UNITO | 01632 12347 |
| 4 | Arthur Vickers | 15a Via Main | Chigley | CW1 5ZH | Regno Unito | 01632 22345 |
| 5 | Brice Lambson | 4 Main St | Chigley | CW1 5ZH | REGNO UNITO | 01632 12349 |
Se necessario, è possibile eseguire il mapping di ogni tipo di entità che compone l'aggregato alla propria tabella:
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");
});
});
}
Gli stessi dati vengono quindi archiviati in tre tabelle:
Autori
| ID | Nome |
|---|---|
| 1 | Maddy Montaquila |
| 2 | Jeremy Likness |
| 3 | Daniel Roth |
| 4 | Arthur Vickers |
| 5 | Brice Lambson |
Contatti
| AuthorId | Telefono |
|---|---|
| 1 | 01632 12345 |
| 2 | 01632 12346 |
| 3 | 01632 12347 |
| 4 | 01632 22345 |
| 5 | 01632 12349 |
Indirizzi
| DettagliContattoIdAutore | Via | Città | Codice postale | Paese |
|---|---|---|---|---|
| 1 | 1 Principale St | Camberwick Green | CW1 5ZH | REGNO UNITO |
| 2 | 2 Main St | Chigley | CW1 5ZH | REGNO UNITO |
| 3 | 3 Principale St | Camberwick Green | CW1 5ZH | REGNO UNITO |
| 4 | 15a Via Main | Chigley | CW1 5ZH | Regno Unito |
| 5 | 4 Main St | Chigley | CW1 5ZH | REGNO UNITO |
Ora, per la parte interessante. In EF7 il ContactDetails tipo di aggregazione può essere mappato a una colonna JSON. Questa operazione richiede una sola chiamata a ToJson() quando si configura il tipo di aggregazione:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().OwnsOne(
author => author.Contact, ownedNavigationBuilder =>
{
ownedNavigationBuilder.ToJson();
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
});
}
La Authors tabella ora conterrà una colonna JSON per ContactDetails, che include un documento JSON per ogni autore:
Autori
| ID | Nome | Contatto |
|---|---|---|
| 1 | Maddy Montaquila | {
} "Telefono":"01632 12345" "Indirizzo": { "Città":"Camberwick Green" "Country":"UK", "Postcode":"CW1 5ZH", "Via":"1 Via Principale" } } |
| 2 | Jeremy Likness | {
} "Telefono":"01632 12346" "Indirizzo": { "City":"Chigley", "Country":"UK", "Postcode":"CH1 5ZH", "Strada":"2 Main St" } } |
| 3 | Daniel Roth | {
} "Telefono":"01632 12347" "Indirizzo": { "Città":"Camberwick Green" "Country":"UK", "Postcode":"CW1 5ZH", "Street":"3 Via principale" } } |
| 4 | Arthur Vickers | {
} "Telefono":"01632 12348" "Indirizzo": { "City":"Chigley", "Country":"UK", "Postcode":"CH1 5ZH", "Strada":"Via Principale 15a" } } |
| 5 | Brice Lambson | {
} "Telefono":"01632 12349" "Indirizzo": { "City":"Chigley", "Country":"UK", "Postcode":"CH1 5ZH", "Street":"4 Main St" } } |
Suggerimento
Questo uso di aggregazioni è molto simile al modo in cui i documenti JSON vengono mappati quando si usa il provider EF Core per Azure Cosmos DB. Le colonne JSON offrono le funzionalità dell'uso di EF Core nei database di documenti nei documenti incorporati in un database relazionale.
I documenti JSON illustrati in precedenza sono molto semplici, ma questa funzionalità di mapping può essere usata anche con strutture di documenti più complesse. Si consideri ad esempio un altro tipo di aggregazione del modello di esempio, usato per rappresentare i metadati relativi a un post:
public class PostMetadata
{
public PostMetadata(int views)
{
Views = views;
}
public int Views { get; set; }
public List<SearchTerm> TopSearches { get; } = new();
public List<Visits> TopGeographies { get; } = new();
public List<PostUpdate> Updates { get; } = new();
}
public class SearchTerm
{
public SearchTerm(string term, int count)
{
Term = term;
Count = count;
}
public string Term { get; private set; }
public int Count { get; private set; }
}
public class Visits
{
public Visits(double latitude, double longitude, int count)
{
Latitude = latitude;
Longitude = longitude;
Count = count;
}
public double Latitude { get; private set; }
public double Longitude { get; private set; }
public int Count { get; private set; }
public List<string>? Browsers { get; set; }
}
public class PostUpdate
{
public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
{
PostedFrom = postedFrom;
UpdatedOn = updatedOn;
}
public IPAddress PostedFrom { get; private set; }
public string? UpdatedBy { get; init; }
public DateTime UpdatedOn { get; private set; }
public List<Commit> Commits { get; } = new();
}
public class Commit
{
public Commit(DateTime committedOn, string comment)
{
CommittedOn = committedOn;
Comment = comment;
}
public DateTime CommittedOn { get; private set; }
public string Comment { get; set; }
}
Questo tipo di aggregazione contiene diversi tipi e raccolte annidati. Le chiamate a OwnsOne e OwnsMany vengono usate per eseguire il mapping di questo tipo di aggregazione:
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));
});
Suggerimento
ToJson è necessario solo nella radice di aggregazione per eseguire il mapping dell'intera aggregazione a un documento JSON.
Con questo mapping, EF7 può creare ed eseguire query in un documento JSON complesso simile al seguente:
{
"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"
}
]
}
]
}
Nota
Il mapping dei tipi spaziali direttamente a JSON non è ancora supportato. Il documento precedente usa i double valori come soluzione alternativa. Vota per supportare i tipi spaziali nelle colonne JSON se è qualcosa che ti interessa.
Nota
Il mapping di raccolte di tipi primitivi a JSON non è ancora supportato. Il documento precedente usa un convertitore di valori per trasformare la raccolta in una stringa delimitata da virgole. Vota per Json: aggiungere il supporto per la collezione di tipi primitivi se ti interessa.
Nota
Il mapping dei tipi di possesso in JSON non è ancora supportato insieme all'ereditarietà TPT o TPC. Vota per Supporto delle proprietà JSON con mapping di ereditarietà TPT/TPC se ciò è di tuo interesse.
Query nelle colonne JSON
Le query nelle colonne JSON funzionano esattamente come l'esecuzione di query in qualsiasi altro tipo di aggregazione in EF Core. Vale a dire, basta usare LINQ! Di seguito sono riportati alcuni esempi.
Query per tutti gli autori che risiedono in Chigley:
var authorsInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();
Questa query genera il codice SQL seguente quando si usa 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'
Si noti l'uso di JSON_VALUE per ottenere il City dal Address all'interno del documento JSON.
Select può essere usato per estrarre e proiettare gli elementi dal documento JSON:
var postcodesInChigley = await context.Authors
.Where(author => author.Contact.Address.City == "Chigley")
.Select(author => author.Contact.Address.Postcode)
.ToListAsync();
Questa query genera il codice SQL seguente:
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'
Ecco un esempio che esegue un po' di più nel filtro e nella proiezione e ordina anche in base al numero di telefono nel documento JSON:
var orderedAddresses = await context.Authors
.Where(
author => (author.Contact.Address.City == "Chigley"
&& author.Contact.Phone != null)
|| author.Name.StartsWith("D"))
.OrderBy(author => author.Contact.Phone)
.Select(
author => author.Name + " (" + author.Contact.Address.Street
+ ", " + author.Contact.Address.City
+ " " + author.Contact.Address.Postcode + ")")
.ToListAsync();
Questa query genera il codice SQL seguente:
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))
Quando il documento JSON contiene raccolte, è possibile proiettarli nei risultati:
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();
Questa query genera il codice SQL seguente:
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
Nota
Le query più complesse che coinvolgono le raccolte JSON richiedono jsonpath supporto. Votare per supportare l'esecuzione di query jsonpath se si tratta di un elemento a cui si è interessati.
Suggerimento
Prendere in considerazione la creazione di indici per migliorare le prestazioni delle query nei documenti JSON. Ad esempio, vedere Indice dei dati JSON quando si usa SQL Server.
Aggiornamento delle colonne JSON
SaveChanges e SaveChangesAsync funzionano normalmente per apportare aggiornamenti a una colonna JSON. Per modifiche estese, l'intero documento verrà aggiornato. Ad esempio, sostituire la maggior parte del Contact documento di un autore:
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();
In questo caso, l'intero nuovo documento viene passato come parametro:
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']
Che viene quindi usato in UPDATE SQL:
UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;
Tuttavia, se viene modificato solo un documento secondario, EF Core userà un JSON_MODIFY comando per aggiornare solo il documento secondario. Ad esempio, modificando l'oggetto Address all'interno di un Contact documento:
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
await context.SaveChangesAsync();
Genera i parametri seguenti:
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']
Che viene utilizzato in UPDATE tramite un'JSON_MODIFY invocazione:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;
Infine, se viene modificata solo una singola proprietà, EF Core userà di nuovo un comando "JSON_MODIFY", questa volta per applicare patch solo al valore della proprietà modificata. Ad esempio:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
Genera i parametri seguenti:
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']
Quali vengono usati di nuovo con JSON_MODIFY:
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
ExecuteUpdate e ExecuteDelete (aggiornamenti in blocco)
Per impostazione predefinita, EF Core tiene traccia delle modifiche apportate alle entità e quindi invia aggiornamenti al database quando viene chiamato uno dei SaveChanges metodi. Le modifiche vengono inviate solo per le proprietà e le relazioni effettivamente modificate. Inoltre, le entità rilevate rimangono sincronizzate con le modifiche inviate al database. Questo meccanismo è un modo efficiente e pratico per inviare inserimenti, aggiornamenti ed eliminazioni per utilizzo generico al database. Queste modifiche vengono inoltre raggruppate per ridurre il numero di round trip del database.
Tuttavia, a volte è utile eseguire comandi di aggiornamento o eliminazione nel database senza coinvolgere lo strumento di rilevamento delle modifiche. EF7 consente di eseguire questa operazione con i nuovi metodi ExecuteUpdate e ExecuteDelete. Questi metodi vengono applicati a una query LINQ e aggiorneranno o elimineranno le entità nel database in base ai risultati della query. Molte entità possono essere aggiornate con un singolo comando e le entità non vengono caricate in memoria, il che significa che ciò può comportare aggiornamenti ed eliminazioni più efficienti.
Tenere tuttavia presente che:
- Le modifiche specifiche da apportare devono essere specificate in modo esplicito; non vengono rilevati automaticamente da EF Core.
- Tutte le entità rilevate non verranno mantenute sincronizzate.
- Potrebbe essere necessario inviare comandi aggiuntivi nell'ordine corretto in modo da non violare i vincoli del database. Ad esempio, l'eliminazione di dipendenti prima che un principale possa essere eliminato.
Tutto ciò significa che ExecuteUpdate e ExecuteDelete i metodi si integrano, anziché sostituire il meccanismo esistente SaveChanges.
Esempi di base ExecuteDelete
Suggerimento
Il codice illustrato di seguito proviene da ExecuteDeleteSample.cs.
Chiamando ExecuteDelete o ExecuteDeleteAsync su un DbSet elimina immediatamente tutte le entità di quel DbSet dal database. Ad esempio, per eliminare tutte le Tag entità:
await context.Tags.ExecuteDeleteAsync();
In questo modo viene eseguito il codice SQL seguente quando si usa SQL Server:
DELETE FROM [t]
FROM [Tags] AS [t]
Più interessante, la query può contenere un filtro. Ad esempio:
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();
In questo modo viene eseguito il codice SQL seguente:
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'
La query può anche usare filtri più complessi, inclusi gli spostamenti ad altri tipi. Ad esempio, per eliminare i tag solo dai vecchi post di blog:
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();
Che esegue:
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))
Esempi di base ExecuteUpdate
Suggerimento
Il codice illustrato di seguito proviene da ExecuteUpdateSample.cs.
ExecuteUpdate e ExecuteUpdateAsync si comportano in modo molto simile ai ExecuteDelete metodi. La differenza principale è che un aggiornamento richiede conoscere le proprietà da aggiornare e come aggiornarle. Questo risultato viene ottenuto usando una o più chiamate a SetProperty. Ad esempio, per aggiornare l'elemento Name di ogni blog:
await context.Blogs.ExecuteUpdateAsync(
s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));
Il primo parametro di SetProperty specifica la proprietà da aggiornare; in questo caso, Blog.Name. Il secondo parametro specifica come calcolare il nuovo valore; in questo caso, prendendo il valore esistente e accodando "*Featured!*". Il codice SQL risultante è:
UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]
Come con ExecuteDelete, la query può essere usata per filtrare le entità aggiornate. Inoltre, è possibile usare più chiamate a SetProperty per aggiornare più proprietà nell'entità di destinazione. Ad esempio, per aggiornare il Title e il Content di tutti i post pubblicati prima del 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 + ")"));
In questo caso, SQL generato è un po' più complicato:
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
Infine, di nuovo come con ExecuteDelete, il filtro può fare riferimento ad altre tabelle. Ad esempio, per aggiornare tutti i tag dai vecchi post:
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
Che genera:
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))
Per altre informazioni ed esempi di codice su ExecuteUpdate e ExecuteDelete, vedere ExecuteUpdate e ExecuteDelete.
Ereditarietà e più tabelle
ExecuteUpdate e ExecuteDelete possono agire solo su una singola tabella. Ciò ha implicazioni quando si lavora con diverse strategie di mapping di ereditarietà. In genere, non ci sono problemi quando si usa la strategia di mapping TPH, poiché è presente una sola tabella da modificare. Ad esempio, eliminando tutte le FeaturedPost entità:
await context.Set<FeaturedPost>().ExecuteDeleteAsync();
Genera il codice SQL seguente quando si usa il mapping TPH:
DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'
In questo caso non sono presenti problemi anche quando si usa la strategia di mapping TPC, poiché sono necessarie solo modifiche a una singola tabella:
DELETE FROM [f]
FROM [FeaturedPosts] AS [f]
Tuttavia, il tentativo di eseguire questa operazione quando si usa la strategia di mapping TPT avrà esito negativo perché richiederebbe l'eliminazione di righe da due tabelle diverse.
L'aggiunta di un filtro alla query spesso indica che l'operazione avrà esito negativo con entrambe le strategie TPC e TPT. Questo è nuovamente dovuto al fatto che le righe potrebbero dover essere eliminate da più tabelle. Ad esempio, la query seguente:
await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();
Genera il codice SQL seguente quando si usa 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%')
Ma fallisce quando si usa TPC o TPT.
Suggerimento
Il problema 10879 tiene traccia dell'aggiunta del supporto per l'invio automatico di più comandi in questi scenari. Vota per questa questione se desideri vederla implementata.
ExecuteDelete e relazioni
Come accennato in precedenza, potrebbe essere necessario eliminare o aggiornare le entità dipendenti prima che l'entità di una relazione possa essere eliminata. Ad esempio, ogni Post oggetto è dipendente dal relativo oggetto associato Author. Ciò significa che un autore non può essere eliminato se un post vi fa ancora riferimento; in questo modo verrà violato il vincolo di chiave esterna nel database. Ad esempio, provando a eseguire questa operazione:
await context.Authors.ExecuteDeleteAsync();
Verrà generata l'eccezione seguente in SQL Server:
Microsoft.Data.SqlClient.SqlException (0x80131904): l'istruzione DELETE è in conflitto con il vincolo REFERENCE "FK_Posts_Authors_AuthorId". Il conflitto si è verificato nel database "TphBlogsContext", tabella "dbo. Post", colonna 'AuthorId'. Il comando è stato terminato.
Per risolvere questo problema, è prima necessario eliminare i post o eliminare la relazione tra ogni post e il relativo autore impostando la AuthorId proprietà della chiave esterna su Null. Ad esempio, usando l'opzione delete:
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
Suggerimento
TagWith può essere usato per contrassegnare ExecuteDelete o ExecuteUpdate allo stesso modo in cui contrassegna le normali query.
Ciò comporta due comandi separati; il primo a eliminare i dipendenti:
-- Deleting posts...
DELETE FROM [p]
FROM [Posts] AS [p]
E il secondo per eliminare i principali:
-- Deleting authors...
DELETE FROM [a]
FROM [Authors] AS [a]
Importante
Per impostazione predefinita, comandi ExecuteDelete e ExecuteUpdate multipli non saranno contenuti in una singola transazione. Tuttavia, le API di transazione DbContext possono essere usate normalmente per eseguire il wrapping di questi comandi in una transazione.
Suggerimento
L'invio di questi comandi in un singolo round trip dipende dal problema 10879. Vota per questa questione se desideri vederla implementata.
La configurazione delle eliminazioni a catena nel database può essere molto utile qui. Nel modello è necessaria la relazione tra Blog e Post , che determina la configurazione di un'eliminazione a catena da parte di EF Core per convenzione. Ciò significa che quando un blog viene eliminato dal database, verranno eliminati anche tutti i post dipendenti. Quindi segue che per eliminare tutti i blog e i post è necessario eliminare solo i blog:
await context.Blogs.ExecuteDeleteAsync();
In questo modo viene restituito il codice SQL seguente:
DELETE FROM [b]
FROM [Blogs] AS [b]
Che, poiché sta eliminando un blog, causerà anche l'eliminazione di tutti i post correlati tramite l'eliminazione a catena configurata.
SaveChanges più veloce
In EF7 le prestazioni di SaveChanges e SaveChangesAsync sono state notevolmente migliorate. In alcuni scenari, il salvataggio delle modifiche è ora fino a quattro volte più veloce rispetto a EF Core 6.0.
La maggior parte di questi miglioramenti deriva da:
- Esecuzione di un minor numero di round trip nel database
- Generazione di SQL più veloce
Di seguito sono riportati alcuni esempi di questi miglioramenti.
Nota
Per una descrizione approfondita di queste modifiche, vedere Annuncio di Entity Framework Core 7 Preview 6: Performance Edition nel blog di .NET.
Suggerimento
Il codice illustrato di seguito proviene da SaveChangesPerformanceSample.cs.
Le transazioni non desiderate vengono eliminate
Tutti i database relazionali moderni garantiscono la transazionalità per le singole istruzioni SQL (la maggior parte). Ovvero, l'istruzione non verrà mai completata solo parzialmente, anche se si verifica un errore. EF7 evita l'avvio di una transazione esplicita in questi casi.
Ad esempio, esaminando il log per la chiamata seguente a SaveChanges:
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
Mostra che in EF Core 6.0 il comando INSERT è avvolto dai comandi per iniziare e poi eseguire il commit di una transazione.
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 rileva che la transazione non è necessaria qui e quindi rimuove queste chiamate:
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);
In questo modo vengono rimossi due round trip del database, che possono fare un'enorme differenza per le prestazioni complessive, soprattutto quando la latenza delle chiamate al database è elevata. Nei sistemi di produzione tipici, il database non si trova nello stesso computer dell'applicazione. Ciò significa che la latenza è spesso relativamente elevata, rendendo questa ottimizzazione particolarmente efficace nei sistemi di produzione reali.
Miglioramento di SQL per l'inserimento di identità semplice
Il caso precedente inserisce una singola riga con una IDENTITY colonna chiave e nessun altro valore generato dal database. EF7 semplifica sql in questo caso usando OUTPUT INSERTED. Sebbene questa semplificazione non sia valida per molti altri casi, è comunque importante migliorare poiché questo tipo di inserimento a riga singola è molto comune in molte applicazioni.
Inserimento di più righe
In EF Core 6.0 l'approccio predefinito per l'inserimento di più righe è stato determinato dalle limitazioni nel supporto di SQL Server per le tabelle con trigger. Volevamo assicurarsi che l'esperienza predefinita funzionasse anche per la minoranza di utenti con trigger nelle tabelle. Ciò significa che non è stato possibile usare una clausola semplice OUTPUT perché, in SQL Server, questo non funziona con i trigger. Al contrario, quando si inseriscono più entità, EF Core 6.0 ha generato alcuni SQL abbastanza convoluti. Ad esempio, questa chiamata a SaveChanges:
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
Le seguenti azioni vengono eseguite quando si eseguono su SQL Server con EF Core versione 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.
Importante
Anche se questo è complicato, l'invio in batch di più inserimenti come questo è ancora molto più veloce rispetto all'invio di un singolo comando per ogni inserimento.
In EF7 è comunque possibile ottenere questo codice SQL se le tabelle contengono trigger, ma per il caso comune ora si generano comandi molto più efficienti, se ancora piuttosto complessi:
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;
La transazione non è più disponibile, come nel caso di inserimento singolo, perché MERGE è una singola istruzione protetta da una transazione implicita. Inoltre, la tabella temporanea non è più disponibile e la clausola OUTPUT invia ora gli ID generati direttamente al client. Questo può essere quattro volte più veloce rispetto a EF Core 6.0, a seconda di fattori ambientali, ad esempio la latenza tra l'applicazione e il database.
Trigger
Se la tabella include trigger, la chiamata a SaveChanges nel codice precedente genererà un'eccezione:
Eccezione non gestita. Microsoft.EntityFrameworkCore.DbUpdateException:
impossibile salvare le modifiche perché la tabella di destinazione include trigger di database. Si prega di configurare il tipo di entità di conseguenza. Per ulteriori informazioni, vederehttps://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
la tabella di destinazione 'BlogsWithTriggers' dell'istruzione DML non può avere trigger abilitati se l'istruzione contiene una clausola OUTPUT senza clausola INTO.
Il codice seguente può essere usato per informare EF Core che la tabella ha un trigger:
modelBuilder
.Entity<BlogWithTrigger>()
.ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));
EF7 eseguirà quindi il ripristino di EF Core 6.0 SQL quando si inviano comandi di inserimento e aggiornamento per questa tabella.
Per ulteriori informazioni, inclusa una convenzione per configurare automaticamente tutte le tabelle mappate con trigger, vedere Le tabelle di SQL Server con trigger ora richiedono una configurazione speciale in EF Core nella documentazione relativa alle modifiche che interrompono la compatibilità di EF7.
Meno roundtrip per l'inserimento di grafici
Prendere in considerazione l'inserimento di un grafico di entità contenente una nuova entità principale e anche nuove entità dipendenti con chiavi esterne che fanno riferimento alla nuova entità. Ad esempio:
await context.AddAsync(
new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();
Se la chiave primaria dell'entità principale viene generata dal database, il valore da impostare per la chiave esterna nell'entità dipendente non è noto fino a quando l'entità principale non viene inserita. EF Core genera due roundtrip per questo: uno per inserire l'entità principale e recuperare la nuova chiave primaria e un secondo per inserire le entità dipendenti con il valore della chiave esterna impostato. Poiché sono disponibili due istruzioni per questa operazione, è necessaria una transazione, vale a dire che sono presenti quattro round trip totali:
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.
Tuttavia, in alcuni casi, il valore della chiave primaria è noto prima dell'inserimento del principale. Questo include:
- Valori chiave non generati automaticamente
- Valori chiave generati nel client, ad esempio Guid chiavi
- Valori chiave generati nel server in batch, ad esempio quando si usa un generatore di valori hi-lo
In EF7, questi casi sono ora ottimizzati in un singolo round trip. Ad esempio, nel caso precedente in SQL Server, la Blog.Id chiave primaria può essere configurata per usare la strategia di generazione hi-lo:
modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();
La SaveChanges chiamata dall'alto è ora ottimizzata per un unico ciclo di andata e ritorno negli inserimenti.
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.
Si noti che una transazione è ancora necessaria qui. Ciò è dovuto al fatto che gli inserimenti vengono eseguiti in due tabelle separate.
EF7 usa anche un singolo batch in altri casi in cui EF Core 6.0 creerebbe più di uno. Ad esempio, quando si eliminano e si inseriscono righe nella stessa tabella.
Valore di SaveChanges
Come illustrato in alcuni esempi, il salvataggio dei risultati nel database può essere un'attività complessa. Questo è il caso in cui l'uso di qualcosa di simile a EF Core ne mostri davvero il valore. EF Core:
- Raggruppa più comandi di inserimento, aggiornamento ed eliminazione per ridurre i round trip
- Indica se è necessaria o meno una transazione esplicita
- Determina l'ordine di inserimento, aggiornamento ed eliminazione di entità in modo che i vincoli del database non vengano violati
- Assicura che i valori generati dal database vengano restituiti in modo efficiente e propagati nuovamente nelle entità
- Imposta automaticamente i valori di chiave esterna usando i valori generati per le chiavi primarie
- Rilevare i conflitti di concorrenza
Inoltre, diversi sistemi di database richiedono SQL diverso per molti di questi casi. Il provider di database EF Core funziona con EF Core per garantire l'invio di comandi corretti ed efficienti per ogni caso.
Mapping di ereditarietà di tipo tabella per concreto (TPC)
Per impostazione predefinita, EF Core esegue il mapping di una gerarchia di ereditarietà dei tipi .NET a una singola tabella di database. Questa operazione è nota come strategia di mapping Table-Per-Hierarchy (TPH). EF Core 5.0 ha introdotto la strategia di tabella per tipo (TPT), che supporta il mapping di ogni tipo .NET a una tabella di database diversa. EF7 introduce la strategia TPC (table-per-concrete-type). TPC esegue anche il mapping dei tipi .NET a tabelle diverse, ma in modo da risolvere alcuni problemi di prestazioni comuni con la strategia TPT.
Suggerimento
Il codice illustrato di seguito proviene da TpcInheritanceSample.cs.
Suggerimento
Il team EF ha mostrato e discusso in dettaglio il mapping TPC in un episodio di .NET Data Community Standup. Come per tutti gli episodi community standup, è possibile guardare l'episodio TPC ora su YouTube.
Schema del database TPC
La strategia TPC è simile alla strategia TPT, ad eccezione del fatto che viene creata una tabella diversa per ogni tipo concreto nella gerarchia, ma le tabelle non vengono create per i tipi astratti, quindi il nome "table-per-concrete-type". Come per TPT, la tabella stessa indica il tipo dell'oggetto salvato. Tuttavia, a differenza del mapping TPT, ogni tabella contiene colonne per ogni proprietà nel tipo concreto e nei relativi tipi di base. Gli schemi del database TPC vengono denormalizzati.
Ad esempio, considera la mappatura di questa gerarchia.
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>"}";
}
Quando si usa SQL Server, le tabelle create per questa gerarchia sono:
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]));
Si noti che:
Non sono presenti tabelle per i
Animaltipi oPet, poiché si trovanoabstractnel modello a oggetti. Tenere presente che C# non consente istanze di tipi astratti e non esiste quindi alcuna situazione in cui un'istanza di tipo astratta verrà salvata nel database.La mappatura delle proprietà nei tipi di base viene ripetuta per ogni tipo concreto. Ad esempio, ogni tabella ha una colonna
Namee sia Gatti che Cani hanno una colonnaVet.Il salvataggio di alcuni dati in questo database comporta quanto segue:
Tavolo dei gatti
| ID | Nome | FoodId | Veterinario | Livello di istruzione |
|---|---|---|---|---|
| 1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA |
| 2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Scuola materna |
| 8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Tabella dei Cani
| ID | Nome | FoodId | Veterinario | FavoriteToy |
|---|---|---|---|---|
| 3 | Avviso popup | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Signor Scoiattolo |
Tabella FarmAnimals
| ID | Nome | FoodId | Valore | Specie |
|---|---|---|---|---|
| 4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100,00 | Equus africanus asinus |
Tabella Degli esseri umani
| ID | Nome | FoodId | IdAnimalePreferito |
|---|---|---|---|
| 5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
| 6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
| 9 | Katie | Null | 8 |
Si noti che, a differenza del mapping TPT, tutte le informazioni per un singolo oggetto sono contenute in una singola tabella. A differenza del mapping TPH, inoltre, non esiste una combinazione di colonna e riga in nessuna tabella che non venga utilizzata dal modello. Di seguito verrà illustrato come queste caratteristiche possono essere importanti per le query e l'archiviazione.
Configurazione dell'ereditarietà TPC
Tutti i tipi in una gerarchia di ereditarietà devono essere inclusi in modo esplicito nel modello quando si esegue il mapping della gerarchia con EF Core. È possibile farlo creando proprietà DbSet sul tuo DbContext per ogni tipo:
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>();
In alternativa, usando il Entity metodo in OnModelCreating:
modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();
Importante
Questo comportamento è diverso dal comportamento di EF6 legacy, in cui i tipi derivati di tipi di base mappati verrebbero individuati automaticamente se fossero contenuti nello stesso assembly.
Non è necessario eseguire altre operazioni per eseguire il mapping della gerarchia come TPH, perché è la strategia predefinita. Tuttavia, a partire da EF7, TPH può essere reso esplicito chiamando UseTphMappingStrategy sul tipo di base della gerarchia:
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
Per usare invece TPT, modificare questo valore in UseTptMappingStrategy:
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
Analogamente, UseTpcMappingStrategy viene usato per configurare il TPC:
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
In ogni caso, il nome della tabella usato per ogni tipo viene ricavato dal nome della DbSet proprietà in DbContextoppure può essere configurato usando il ToTable metodo builder o l'attributo [Table] .
Prestazioni delle query TPC
Per le query, la strategia TPC è un miglioramento rispetto al TPT perché garantisce che le informazioni per una determinata istanza di entità vengano sempre archiviate in una singola tabella. Ciò significa che la strategia TPC può essere utile quando la gerarchia mappata è grande e ha molti tipi concreti (in genere foglia), ognuno con un numero elevato di proprietà e dove nella maggior parte delle query vengono usati solo un piccolo subset di tipi.
Il codice SQL generato per tre semplici query LINQ può essere usato per osservare dove TPC funziona bene rispetto a TPH e TPT. Queste query sono:
Query che restituisce entità di tutti i tipi nella gerarchia:
context.Animals.ToList();Query che restituisce entità da un subset di tipi nella gerarchia:
context.Pets.ToList();Query che restituisce solo entità da un singolo tipo foglia nella gerarchia:
context.Cats.ToList();
TPH Query
Quando si utilizza TPH, tutte e tre le query interrogano una singola tabella, ma con filtri diversi applicati sulla colonna discriminante.
TPH SQL che restituisce entità di tutti i tipi nella gerarchia:
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 che restituisce entità da un subset di tipi nella gerarchia:
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 che restituisce solo entità di un singolo tipo di foglia nella gerarchia:
SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel] FROM [Animals] AS [a] WHERE [a].[Discriminator] = N'Cat'
Tutte queste query devono essere eseguite correttamente, soprattutto con un indice di database appropriato nella colonna discriminatoria.
Query di TPT
Quando si usa TPT, tutte queste query richiedono l'unione di più tabelle, poiché i dati per un determinato tipo concreto vengono suddivisi in più tabelle:
TPT SQL che restituisce entità di tutti i tipi nella gerarchia:
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 che restituisce entità da un subset di tipi nella gerarchia:
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 che restituisce solo entità da un singolo tipo di foglia nella gerarchia.
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]
Nota
EF Core usa la "sintesi discriminatoria" per determinare la tabella da cui provengono i dati e quindi il tipo corretto da usare. Ciò funziona perché LEFT JOIN restituisce i valori Null per la colonna ID dipendente (le "tabelle secondarie") che non sono il tipo corretto. Quindi, per un cane, [d].[Id] sarà diverso da null e tutti gli altri ID (concreti) saranno null.
Tutte queste query possono subire problemi di prestazioni a causa dei join delle tabelle. Questo è il motivo per cui TPT non è mai una scelta ottimale per le prestazioni delle query.
Query TPC
Il TPC migliora il TPT per tutte queste query perché il numero di tabelle su cui eseguire query è ridotto. Inoltre, i risultati di ogni tabella vengono combinati usando UNION ALL, che può essere notevolmente più veloce rispetto a un join di tabella, poiché non è necessario eseguire alcuna corrispondenza tra righe o deduplicazione di righe.
TPC SQL che restituisce entità di tutti i tipi nella gerarchia:
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 che restituisce entità da un subset di tipi nella gerarchia:
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 che restituisce solo entità da un singolo tipo foglia nella gerarchia:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel] FROM [Cats] AS [c]
Anche se TPC è migliore di TPT per tutte queste query, le query TPH sono ancora migliori quando restituiscono istanze di più tipi. Questo è uno dei motivi per cui TPH è la strategia predefinita usata da EF Core.
Come illustrato in SQL per la query n. 3, il TPC è davvero eccellente quando si eseguono query per le entità di un singolo tipo foglia. La query usa solo una singola tabella e non richiede filtri.
Inserimenti e aggiornamenti TPC
Il TPC offre anche prestazioni elevate durante il salvataggio di una nuova entità, poiché ciò richiede l'inserimento di una sola riga in una singola tabella. Questo vale anche per il TPH. Con TPT, le righe devono essere inserite in molte tabelle, con prestazioni meno buone.
Lo stesso vale spesso per gli aggiornamenti, anche se in questo caso tutte le colonne aggiornate si trovano nella stessa tabella, anche per TPT, la differenza potrebbe non essere significativa.
Considerazioni sullo spazio
Sia TPT che TPC possono usare meno spazio di archiviazione rispetto al TPH quando sono presenti molti sottotipi con molte proprietà che spesso non vengono usate. Ciò è dovuto al fatto che ogni riga della tabella TPH deve archiviare un oggetto NULL per ognuna di queste proprietà inutilizzate. In pratica, questo è raramente un problema, ma potrebbe essere opportuno considerare quando si archiviano grandi quantità di dati con queste caratteristiche.
Suggerimento
Se il sistema di database lo supporta (e.g. SQL Server), considerare l'uso di "colonne sparse" per le colonne TPH che verranno popolate raramente.
Generazione della chiave
La strategia di mapping dell'ereditarietà scelta ha conseguenze sulla modalità di generazione e gestione dei valori di chiave primaria. Le chiavi in TPH sono facili poiché ogni istanza di entità è rappresentata da una singola riga in una singola tabella. È possibile usare qualsiasi tipo di generazione di valori chiave e non sono necessari vincoli aggiuntivi.
Per la strategia TPT, è sempre presente una riga nella tabella mappata al tipo di base della gerarchia. Qualsiasi tipo di generazione di chiavi può essere usato in questa riga e le chiavi per altre tabelle sono collegate a questa tabella usando vincoli di chiave esterna.
Le cose diventano un po' più complicate per TPC. Prima di tutto, è importante comprendere che EF Core richiede che tutte le entità in una gerarchia abbiano un valore di chiave univoco, anche se le entità hanno tipi diversi. Pertanto, usando il modello di esempio, un cane non può avere lo stesso valore di chiave ID di un gatto. In secondo luogo, a differenza di TPT, non esiste una tabella comune che può fungere da singola posizione in cui i valori chiave risiedono e possono essere generati. Ciò significa che non è possibile utilizzare una colonna semplice Identity .
Per i database che supportano le sequenze, è possibile generare valori di chiave usando una singola sequenza a cui viene fatto riferimento nel vincolo predefinito per ogni tabella. Questa è la strategia usata nelle tabelle TPC illustrate in precedenza, in cui ogni tabella presenta quanto segue:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence è una sequenza di database creata da EF Core. Questa strategia viene usata per impostazione predefinita per le gerarchie TPC quando si usa il provider di database EF Core per SQL Server. I provider di database per altri database che supportano le sequenze devono avere un valore predefinito simile. Altre strategie di generazione chiave che usano sequenze, ad esempio i modelli Hi-Lo, possono essere usate anche con TPC.
Anche se le colonne Identity standard non funzionano con TPC, è possibile usare le colonne Identity se ogni tabella è configurata con un valore di inizializzazione appropriato e un incremento in modo che i valori generati per ogni tabella non siano mai in conflitto. Ad esempio:
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 non supporta sequenze o inizializzazione/incremento dell'identità e pertanto la generazione di valori di chiave integer non è supportata quando si usa SQLite con la strategia TPC. Tuttavia, la generazione lato client o chiavi univoche globali, ad esempio chiavi GUID, sono supportate in qualsiasi database, incluso SQLite.
Vincoli di chiavi esterne
La strategia di mapping TPC crea uno schema SQL denormalizzato. Questo è un motivo per cui alcuni puristi di database sono contrari. Si consideri la colonna FavoriteAnimalId chiave esterna, ad esempio. Il valore in questa colonna deve corrispondere al valore della chiave primaria di alcuni animali. Questa operazione può essere applicata nel database con un vincolo FK semplice quando si usa TPH o TPT. Ad esempio:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Tuttavia, quando si usa il TPC, la chiave primaria per un animale viene archiviata nella tabella per il tipo concreto di tale animale. Ad esempio, la Cats.Id chiave primaria di un gatto viene archiviata nella colonna, mentre la Dogs.Id chiave primaria di un cane viene archiviata nella colonna e così via. Ciò significa che non è possibile creare un vincolo FK per questa relazione.
In pratica, questo non è un problema, purché l'applicazione non tenti di inserire dati non validi. Ad esempio, se tutti i dati vengono inseriti da EF Core e utilizzano le proprietà di navigazione per correlare le entità, è garantito che la colonna FK conterrà sempre un valore PK valido.
Riepilogo e indicazioni
In sintesi, TPC è una buona strategia di mapping da usare quando il codice eseguirà principalmente query per le entità di un singolo tipo foglia. Ciò è dovuto al fatto che i requisiti di archiviazione sono più piccoli e non è presente alcuna colonna discriminatoria che potrebbe richiedere un indice. Anche gli inserimenti e gli aggiornamenti sono efficienti.
Detto questo, TPH è in genere corretto per la maggior parte delle applicazioni ed è un buon valore predefinito per un'ampia gamma di scenari, quindi non aggiungere la complessità del TPC se non è necessario. In particolare, se il codice effettuerà principalmente query per entità di molti tipi, ad esempio scrivere query sul tipo di base, è consigliabile usare TPH per TPC.
Usare TPT solo se vincolato a farlo da fattori esterni.
Modelli di reverse engineering personalizzati
È ora possibile personalizzare il codice scaffolding quando si esegue il reverse engineering di un modello EF da un database. Per iniziare, aggiungere i modelli predefiniti al progetto:
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
I modelli possono quindi essere personalizzati e verranno usati automaticamente da dotnet ef dbcontext scaffold e Scaffold-DbContext.
Per altri dettagli, vedere Modelli di reverse engineering personalizzati.
Suggerimento
Il team ef ha dimostrato e parlato in modo approfondito dei modelli di reverse engineering in un episodio di .NET Data Community Standup. Come per tutti gli episodi di Community Standup, puoi guardare l'episodio dei modelli T4 ora su YouTube.
Convenzioni di costruzione dei modelli
EF Core usa un "modello" di metadati per descrivere il mapping dei tipi di entità dell'applicazione al database sottostante. Questo modello viene compilato usando un set di circa 60 "convenzioni". Il modello compilato dalle convenzioni può quindi essere personalizzato usando gli attributi di mapping (nota come "annotazioni dei dati") e/o chiamate all'API DbModelBuilder in OnModelCreating.
A partire da EF7, le applicazioni possono ora rimuovere o sostituire una di queste convenzioni, nonché aggiungere nuove convenzioni. Le convenzioni di compilazione dei modelli sono un modo efficace per controllare la configurazione del modello, ma può essere complesso e difficile da ottenere correttamente. In molti casi, è possibile usare la configurazione del modello pre-convenzione esistente per specificare facilmente una configurazione comune per proprietà e tipi.
Le modifiche alle convenzioni utilizzate da un DbContext vengono apportate sovrascrivendo il metodo DbContext.ConfigureConventions. Ad esempio:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Suggerimento
Per trovare tutte le convenzioni di compilazione dei modelli predefinite, cercare ogni classe che implementa l'interfaccia IConvention .
Suggerimento
Il codice illustrato di seguito proviene da ModelBuildingConventionsSample.cs.
Rimozione di una convenzione esistente
A volte una delle convenzioni predefinite potrebbe non essere appropriata per l'applicazione, nel qual caso può essere rimossa.
Esempio: Non creare indici per le colonne chiave esterna
In genere è opportuno creare indici per le colonne di chiave esterna (FK) e quindi esiste una convenzione predefinita per questo: ForeignKeyIndexConvention. Esaminando la vista di debug del modello
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
Tuttavia, gli indici hanno un sovraccarico e, come richiesto qui, potrebbe non essere sempre appropriato crearli per tutte le colonne FK. A tale scopo, è possibile rimuovere l'oggetto ForeignKeyIndexConvention durante la compilazione del modello:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
Esaminando adesso la visualizzazione di debug del modello per Post, notiamo che gli indici sugli FK non sono stati creati.
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
Quando si desidera, gli indici possono comunque essere creati in modo esplicito per le colonne chiave esterna, usando IndexAttribute:
[Index("BlogId")]
public class Post
{
// ...
}
Oppure con la configurazione in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}
Esaminando di nuovo il Post tipo di entità, ora contiene l'indice BlogId , ma non l'indice 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
Suggerimento
Se il modello non usa attributi di mapping (ovvero annotazioni di dati) per la configurazione, tutte le convenzioni che terminano AttributeConvention possono essere rimosse in modo sicuro per velocizzare la compilazione del modello.
Aggiunta di una nuova convenzione
La rimozione delle convenzioni esistenti è un inizio, ma cosa accade per aggiungere convenzioni di compilazione di modelli completamente nuove? EF7 supporta anche questo.
Esempio: Vincolare la lunghezza delle proprietà discriminatorie
La strategia di mapping dell'ereditarietà tabella-per-gerarchia richiede una colonna discriminatoria per specificare quale tipo è rappresentato in una determinata riga. Per impostazione predefinita, EF usa una colonna stringa non delimitata per il discriminante, che garantisce che funzioni per qualsiasi lunghezza del discriminante. Tuttavia, vincolare la lunghezza massima delle stringhe discriminatorie può rendere più efficiente l'archiviazione e le query. Si creerà una nuova convenzione che lo farà.
Le convenzioni di compilazione dei modelli di EF Core vengono attivate in base alle modifiche apportate al modello durante la compilazione. In questo modo il modello viene aggiornato man mano che viene eseguita la configurazione esplicita, vengono applicati gli attributi di mapping e vengono eseguite altre convenzioni. Per partecipare a questa operazione, ogni convenzione implementa una o più interfacce che determinano quando verrà attivata la convenzione. Ad esempio, una convenzione che implementa IEntityTypeAddedConvention verrà attivata ogni volta che viene aggiunto un nuovo tipo di entità al modello. Analogamente, una convenzione che implementa sia IForeignKeyAddedConvention sia IKeyAddedConvention verrà attivata ogni volta che viene aggiunta una chiave o una chiave esterna al modello.
Conoscere le interfacce da implementare può essere difficile, poiché la configurazione eseguita al modello in un determinato momento può essere modificata o rimossa in un secondo momento. Ad esempio, una chiave può essere creata per convenzione, ma successivamente sostituita quando viene configurata in modo esplicito una chiave diversa.
Facciamo questo un po ' più concreto facendo un primo tentativo di implementazione della convenzione di lunghezza discriminatoria:
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);
}
}
}
Questa convenzione implementa IEntityTypeBaseTypeChangedConvention, il che significa che verrà attivato ogni volta che viene modificata la gerarchia di ereditarietà mappata per un tipo di entità. La convenzione trova e configura quindi la proprietà discriminatoria della stringa per la gerarchia.
Questa convenzione viene quindi usata chiamando Add in ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new DiscriminatorLengthConvention1());
}
Suggerimento
Anziché aggiungere direttamente un'istanza della convenzione, il Add metodo accetta una factory per la creazione di istanze della convenzione. Questo consente alla convenzione di utilizzare le dipendenze dal provider di servizi interni di EF Core. Poiché questa convenzione non ha dipendenze, il parametro del provider di servizi è denominato _, a indicare che non viene mai usato.
La compilazione del modello e l'analisi del Post tipo di entità mostra che questa proprietà ha funzionato: la proprietà discriminatoria è ora configurata in con una lunghezza massima di 24:
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Ma cosa succede se ora configuriamo in modo esplicito una proprietà discriminatoria diversa? Ad esempio:
modelBuilder.Entity<Post>()
.HasDiscriminator<string>("PostTypeDiscriminator")
.HasValue<Post>("Post")
.HasValue<FeaturedPost>("Featured");
Esaminando la visualizzazione di debug del modello, si scopre che la lunghezza del discriminatorio non è più configurata.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw
Ciò è dovuto al fatto che la proprietà discriminatoria configurata nella convenzione è stata successivamente rimossa quando è stato aggiunto il discriminatorio personalizzato. Potremmo tentare di risolvere questo problema implementando un'altra interfaccia sulla nostra convenzione per reagire alle modifiche discriminatorie, ma capire quale interfaccia implementare non è facile.
Fortunatamente, c'è un modo diverso per affrontare questo che rende le cose molto più facili. Molte volte, non importa che aspetto abbia il modello mentre viene costruito, purché il modello finale sia corretto. Inoltre, la configurazione da applicare spesso non deve attivare altre convenzioni per reagire. Pertanto, la nostra convenzione può implementare IModelFinalizingConvention. Le regole di finalizzazione del modello vengono eseguite dopo il completamento di tutte le altre operazioni di costruzione del modello e quindi hanno accesso allo stato finale del modello. Una convenzione di finalizzazione del modello in genere eseguirà l'iterazione dell'intero modello configurando gli elementi del modello man mano che passa. In questo caso, quindi, troveremo ogni discriminatore nel modello e lo configureremo.
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);
}
}
}
}
Dopo aver compilato il modello con questa nuova convenzione, si scopre che la lunghezza del discriminatorio è ora configurata correttamente anche se è stata personalizzata:
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)
Solo per divertimento, andiamo un ulteriore passo avanti e configuriamo la lunghezza massima in modo che sia la lunghezza del valore discriminatorio più lungo.
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);
}
}
}
}
Ora la lunghezza massima della colonna discriminatoria è 8, ovvero la lunghezza di "In primo piano", il valore discriminatorio più lungo in uso.
PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)
Suggerimento
Ci si potrebbe chiedere se la convenzione deve anche creare un indice per la colonna discriminatoria. In GitHub è disponibile una discussione su questo argomento. La risposta breve è che a volte un indice può essere utile, ma la maggior parte del tempo probabilmente non sarà. Pertanto, è consigliabile creare indici appropriati in base alle esigenze, anziché avere una convenzione per farlo sempre. Ma se non si è d'accordo con questo, la convenzione precedente può essere facilmente modificata per creare un indice.
Esempio: lunghezza predefinita per tutte le proprietà stringa
Verrà ora esaminato un altro esempio in cui è possibile usare una convenzione di finalizzazione, impostando una lunghezza massima predefinita per qualsiasi proprietà stringa, come richiesto in GitHub. La convenzione è simile all'esempio precedente:
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);
}
}
}
Questa convenzione è piuttosto semplice. Trova ogni proprietà stringa nel modello e ne imposta la lunghezza massima su 512. Esaminando la visualizzazione di debug nelle proprietà per Post, si noterà che tutte le proprietà stringa hanno ora una lunghezza massima di 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)
Ma la Content proprietà dovrebbe probabilmente consentire più di 512 caratteri, o tutti i nostri post saranno abbastanza brevi! Questa operazione può essere eseguita senza modificare la convenzione configurando in modo esplicito la lunghezza massima per questa proprietà, usando un attributo di mapping:
[MaxLength(4000)]
public string Content { get; set; }
Oppure con il codice in OnModelCreating:
modelBuilder.Entity<Post>()
.Property(post => post.Content)
.HasMaxLength(4000);
Ora tutte le proprietà hanno una lunghezza massima di 512, ad eccezione di Content, che è stato configurato esplicitamente con 4000.
EntityType: Post
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
AuthorId (no field, int?) Shadow FK Index
BlogId (no field, int) Shadow Required FK Index
Content (string) Required MaxLength(4000)
Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
PublishedOn (DateTime) Required
Title (string) Required MaxLength(512)
Perché quindi la convenzione non ha sostituito la lunghezza massima configurata in modo esplicito? La risposta è che EF Core tiene traccia del modo in cui è stata eseguita ogni parte della configurazione. Questa proprietà è rappresentata dall'enumerazione ConfigurationSource . I diversi tipi di configurazione sono:
-
Explicit: l'elemento del modello è stato configurato in modo esplicito inOnModelCreating -
DataAnnotation: l'elemento del modello è stato configurato usando un attributo di mapping (noto anche come annotazione dei dati) nel tipo CLR -
Convention: l'elemento del modello è stato configurato da una convenzione di compilazione del modello
Le convenzioni non sostituiscono mai la configurazione contrassegnata come DataAnnotation o Explicit. Questo risultato viene ottenuto usando un "generatore di convenzioni", ad esempio , IConventionPropertyBuilderottenuto dalla Builder proprietà . Ad esempio:
property.Builder.HasMaxLength(512);
La chiamata HasMaxLength al generatore di convenzioni imposta la lunghezza massima solo se non è già stata configurata da un attributo di mapping o in OnModelCreating.
I metodi del generatore come questo hanno anche un secondo parametro: fromDataAnnotation. Impostare questa proprietà su true se la convenzione esegue la configurazione per conto di un attributo di mapping. Ad esempio:
property.Builder.HasMaxLength(512, fromDataAnnotation: true);
In questo modo, ConfigurationSource è impostato su DataAnnotation, il che significa che il valore può ora essere sovrascritto tramite mapping esplicito su OnModelCreating, ma non da convenzioni di attributo che non prevedono il mapping.
Infine, prima di lasciare questo esempio, cosa accade se si usano entrambi MaxStringLengthConvention e DiscriminatorLengthConvention3 contemporaneamente? La risposta è che dipende dall'ordine in cui vengono aggiunte, poiché le convenzioni di finalizzazione del modello vengono eseguite nell'ordine in cui vengono aggiunte. Quindi, se MaxStringLengthConvention viene aggiunto per ultimo, verrà eseguito per ultimo e la lunghezza massima della proprietà discriminatoria verrà impostata su 512. Pertanto, in questo caso, è preferibile aggiungere DiscriminatorLengthConvention3 per ultimo in modo che possa sostituire la lunghezza massima predefinita solo per le proprietà discriminatorie, lasciando tutte le altre proprietà stringa come 512.
Sostituzione di una convenzione esistente
In alcuni casi, anziché rimuovere completamente una convenzione esistente, si vuole sostituirla con una convenzione che esegue fondamentalmente la stessa operazione, ma con il comportamento modificato. Ciò è utile perché la convenzione esistente implementerà già le interfacce necessarie in modo da essere attivata in modo appropriato.
Esempio: Mapping delle proprietà di consenso esplicito
EF Core esegue il mapping di tutte le proprietà pubbliche di lettura/scrittura per convenzione. Questo potrebbe non essere appropriato per il modo in cui vengono definiti i tipi di entità. Per cambiare questo, possiamo sostituire il PropertyDiscoveryConvention con la nostra implementazione che non mappa alcuna proprietà a meno che non sia esplicitamente mappata in OnModelCreating o contrassegnata con un nuovo attributo chiamato Persist:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}
Ecco la nuova convenzione:
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;
}
}
}
}
Suggerimento
Quando si sostituisce una convenzione predefinita, la nuova implementazione della convenzione deve ereditare dalla classe convenzione esistente. Si noti che alcune convenzioni hanno implementazioni relazionali o specifiche del provider, nel qual caso la nuova implementazione della convenzione deve ereditare dalla classe di convenzioni esistente più specifica per il provider di database in uso.
La convenzione viene quindi registrata usando il Replace metodo in ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}
Suggerimento
Si tratta di un caso in cui la convenzione esistente presenta dipendenze, rappresentate dall'oggetto ProviderConventionSetBuilderDependencies dipendenza. Questi vengono ottenuti dal provider di servizi interno usando GetRequiredService e passati al costruttore della convenzione.
Questa convenzione funziona ottenendo tutte le proprietà e i campi leggibili dal tipo di entità specificato. Se il membro è attribuito con [Persist], viene eseguito il mapping chiamando:
entityTypeBuilder.Property(memberInfo);
D'altra parte, se il membro è una proprietà che altrimenti sarebbe stata mappata, viene esclusa dal modello utilizzando:
entityTypeBuilder.Ignore(propertyInfo.Name);
Si noti che questa convenzione consente di eseguire il mapping dei campi (oltre alle proprietà) purché siano contrassegnati con [Persist]. Ciò significa che è possibile usare campi privati come chiavi nascoste nel modello.
Si considerino ad esempio i tipi di entità seguenti:
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; }
}
Il modello compilato da questi tipi di entità è:
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
Si noti che normalmente, IsClean sarebbe stato mappato, ma poiché non è contrassegnato con [Persist] (presumibilmente perché la pulizia non è una proprietà persistente del bucato), viene ora considerato come una proprietà non mappata.
Suggerimento
Non è stato possibile implementare questa convenzione come convenzione di finalizzazione del modello perché il mapping di una proprietà attiva molte altre convenzioni per l'esecuzione per configurare ulteriormente la proprietà mappata.
Mappatura delle stored procedure
Per impostazione predefinita, EF Core genera comandi di inserimento, aggiornamento ed eliminazione che funzionano direttamente con tabelle o viste aggiornabili. EF7 introduce il supporto per il mapping di questi comandi alle stored procedure.
Suggerimento
EF Core ha sempre supportato il recupero dei dati tramite procedure memorizzate. Il nuovo supporto in EF7 riguarda in modo esplicito l'uso di stored procedure per inserimenti, aggiornamenti ed eliminazioni.
Importante
Il supporto per la mappatura delle stored procedure non implica che le stored procedure siano consigliate.
Le procedure memorizzate vengono mappate in OnModelCreating tramite InsertUsingStoredProcedure, UpdateUsingStoredProcedure e DeleteUsingStoredProcedure. Ad esempio, per mappare le stored procedure per un tipo di entità 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();
});
Questa configurazione corrisponde alle procedure memorizzate seguenti quando si usa SQL Server:
Per gli inserimenti
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
Per gli aggiornamenti
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
Per le cancellazioni
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
Suggerimento
Non è necessario utilizzare stored procedure per ogni tipo nel modello o per tutte le operazioni su un determinato tipo. Ad esempio, se viene specificato solo DeleteUsingStoredProcedure per un determinato tipo, EF Core genererà SQL come di consueto per le operazioni di inserimento e aggiornamento e userà solo la stored procedure per le eliminazioni.
Il primo argomento passato a ogni metodo è il nome della stored procedure. Può essere omesso, nel qual caso EF Core userà il nome della tabella aggiunto con "_Insert", "_Update" o "_Delete". Pertanto, nell'esempio precedente, poiché la tabella è denominata "People", i nomi delle stored procedure possono essere rimossi senza alcuna modifica delle funzionalità.
Il secondo argomento è un costruttore usato per configurare l'input e l'output della stored procedure, inclusi i parametri, i valori di ritorno e le colonne dei risultati.
Parametri
I parametri devono essere aggiunti al generatore nello stesso ordine in cui vengono visualizzati nella definizione della stored procedure.
Nota
I parametri possono essere denominati, ma EF Core chiama sempre stored procedure usando argomenti posizionali anziché argomenti denominati. Vota per Consenti la configurazione del mapping sproc per utilizzare i nomi dei parametri per l'invocazione se ti interessa la chiamata per nome.
Il primo argomento di ogni metodo del generatore di parametri specifica la proprietà nel modello a cui è associato il parametro. Può trattarsi di un'espressione lambda:
storedProcedureBuilder.HasParameter(a => a.Name);
Oppure una stringa, particolarmente utile quando si esegue la mappatura delle proprietà shadow:
storedProcedureBuilder.HasParameter("Name");
I parametri sono, per impostazione predefinita, configurati per "input". I parametri "Output" o "input/output" possono essere configurati usando un generatore annidato. Ad esempio:
storedProcedureBuilder.HasParameter(
document => document.RetrievedOn,
parameterBuilder => parameterBuilder.IsOutput());
Esistono tre diversi metodi di generatore per diversi tipi di parametri:
-
HasParameterspecifica un parametro normale associato al valore corrente della proprietà specificata. -
HasOriginalValueParameterspecifica un parametro associato al valore originale della proprietà specificata. Il valore originale è il valore della proprietà quando è stata eseguita una query dal database, se noto. Se questo valore non è noto, viene invece usato il valore corrente. I parametri dei valori originali sono utili per i token di concorrenza. -
HasRowsAffectedParameterspecifica un parametro utilizzato per restituire il numero di righe interessate dalla stored procedure.
Suggerimento
I parametri dei valori originali devono essere usati per i valori chiave nelle stored procedure "update" e "delete". In questo modo, la riga corretta verrà aggiornata nelle versioni future di EF Core che supportano valori di chiave modificabili.
Restituzione di valori
EF Core supporta tre meccanismi per la restituzione di valori dalle stored procedure:
- Parametri di output, come illustrato in precedenza.
- Colonne dei risultati, specificate utilizzando il metodo builder
HasResultColumn. - Il valore restituito, limitato alla restituzione del numero di righe interessate e viene specificato utilizzando il
HasRowsAffectedReturnValuemetodo builder.
I valori restituiti dalle stored procedure vengono spesso usati per i valori generati, predefiniti o calcolati, ad esempio da una Identity chiave o da una colonna calcolata. Ad esempio, la configurazione seguente specifica quattro colonne dei risultati:
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);
});
Questi vengono utilizzati per restituire:
- Valore della chiave generato per la
Idproprietà . - Valore predefinito generato dal database per la
FirstRecordedOnproprietà . - Valore calcolato generato dal database per la
RetrievedOnproprietà . - Token di concorrenza generato
rowversionautomaticamente per la proprietàRowVersion.
Questa configurazione esegue il mapping alla seguente stored procedure quando si usa 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
Concorrenza ottimistica
La concorrenza ottimistica funziona allo stesso modo con le stored procedure che senza. La "stored procedure" deve:
- Usare un token di concorrenza in una
WHEREclausola per assicurarsi che la riga venga aggiornata solo se ha un token valido. Il valore usato per il token di concorrenza è in genere, ma non deve essere, il valore originale della proprietà del token di concorrenza. - Restituisce il numero di righe interessate in modo che EF Core possa confrontarlo con il numero previsto di righe interessate e generare un'eccezione
DbUpdateConcurrencyExceptionse i valori non corrispondono.
Ad esempio, la stored procedure di SQL Server seguente usa un rowversion token di concorrenza automatica:
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
Questa operazione è configurata in EF Core usando:
.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();
});
Si noti che:
- Viene usato il valore originale del
RowVersiontoken di concorrenza. - La stored procedure usa una
WHEREclausola per assicurarsi che la riga venga aggiornata solo se ilRowVersionvalore originale corrisponde. - Il nuovo valore generato per il
RowVersionviene inserito in una tabella temporanea. - Vengono restituiti il numero di righe interessate (
@@ROWCOUNT) e il valore generatoRowVersion.
Mapping delle gerarchie di ereditarietà con le stored procedure
EF Core richiede che le routine memorizzate seguano il layout di tabella per i tipi in una gerarchia. Ciò significa che:
- Una gerarchia mappata tramite TPH deve avere una sola stored procedure di inserimento, aggiornamento e/o eliminazione che opera sulla singola tabella mappata. Le procedure memorizzate di inserimento e aggiornamento devono avere un parametro per il valore discriminatore.
- Una gerarchia mappata tramite TPT deve avere una stored procedure di inserimento, aggiornamento e/o eliminazione per ogni tipo, inclusi i tipi astratti. EF Core effettuerà più chiamate in base alle esigenze per aggiornare, inserire ed eliminare righe in tutte le tabelle.
- Una gerarchia mappata tramite TPC deve utilizzare una stored procedure per inserire, aggiornare e/o eliminare per ogni tipo concreto, ma non per i tipi astratti.
Nota
Se usare una singola stored procedure per tipo concreto indipendentemente dalla strategia di mapping è un elemento a cui si è interessati, voti per il supporto all'uso di un singolo sproc per tipo concreto indipendentemente dalla strategia di mappatura dell'ereditarietà.
Mappatura dei tipi di entità posseduti verso le procedure memorizzate
La configurazione delle stored procedure per i tipi posseduti viene eseguita nel generatore di tipi posseduti annidati. Ad esempio:
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();
});
});
Nota
Le procedure di archiviazione per inserimento, aggiornamento ed eliminazione supportano solo tipi di proprietà posseduta, i quali devono essere mappati a tabelle separate. Ovvero, il tipo di proprietà non può essere rappresentato dalle colonne nella tabella proprietaria. Votare per Aggiungere il supporto per la suddivisione "tabella" al mapping sproc CUD se si tratta di una limitazione che si desidera visualizzare rimossa.
Mapping delle entità di join molti-a-molti alle procedure memorizzate
La configurazione delle procedure memorizzate, che uniscono entità molti-a-molti, può essere effettuata come parte della configurazione molti-a-molti. Ad esempio:
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();
});
});
});
Nuovi e migliorati intercettori ed eventi
Gli intercettori di EF Core consentono l'intercettazione, la modifica e/o l'eliminazione delle operazioni di EF Core. EF Core include anche eventi e gestione dei log .NET tradizionali.
EF7 include i miglioramenti seguenti per gli intercettori:
- Intercettazione per la creazione e il popolamento di nuove istanze di entità (anche nota come "materializzazione")
- Intercettazione per modificare l'albero delle espressioni LINQ prima della compilazione di una query
- Intercettazione per la gestione della concorrenza ottimistica (
DbUpdateConcurrencyException) - Intercettazione per le connessioni prima di verificare se la stringa di connessione sia stata impostata
- Intercettazione per quando EF Core ha terminato l'utilizzo di un set di risultati, ma prima che tale set di risultati venga chiuso
- Intercettazione per la creazione di un oggetto
DbConnectionda parte di EF Core - Intercettazione per
DbCommanddopo l'inizializzazione
EF7 include anche nuovi eventi .NET tradizionali per:
- Quando un'entità sta per essere rilevata o modificata lo stato, ma prima che venga effettivamente rilevata o modificata lo stato
- Prima e dopo EF Core rileva le modifiche alle entità e alle proprietà (nota anche come
DetectChangesintercettazione)
Le sezioni seguenti forniscono brevi riepiloghi di queste nuove funzionalità di intercettazione. Per la documentazione dettagliata e gli esempi di codice completi, vedere Intercettori.
Intercettazione della materializzazione
Il nuovo IMaterializationInterceptor supporta l'intercettazione prima e dopo la creazione di un'istanza di entità e prima e dopo l'inizializzazione delle proprietà di tale istanza. L'intercettore può modificare o sostituire l'istanza dell'entità in ogni punto. In questo modo è possibile impostare proprietà non mappate, usando una factory per creare istanze, inserendo servizi in entità e altro ancora.
Ad esempio, un intercettore può impostare un Retrieved timestamp sulle entità quando vengono caricate dal database:
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
Per altre informazioni ed esempi, tra cui l'inserimento di servizi in entità, vedere Materializzazione dell'intercettazione.
Intercettazione dell'albero delle espressioni LINQ
Il nuovo IQueryExpressionInterceptor consente all'albero delle espressioni LINQ di intercettare e modificare una query prima della compilazione. Può essere usato per aggiungere dinamicamente l'ordinamento secondario, applicare filtri o apportare altre modifiche alle query nell'applicazione.
Ad esempio, l'intercettore seguente sostituisce l'albero delle espressioni prima della compilazione:
public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
=> new KeyOrderingExpressionVisitor().Visit(queryExpression);
// ExpressionVisitor that modifies the tree...
}
Per altre informazioni ed esempi, vedere Intercettazione di espressioni di query.
Intercettazione della concorrenza ottimistica
ISaveChangesInterceptor ora ha i metodi ThrowingConcurrencyException e ThrowingConcurrencyExceptionAsync che vengono chiamati prima che venga lanciata un'eccezione DbUpdateConcurrencyException. Questi punti di intercettazione consentono di eliminare l'eccezione, possibilmente abbinata a modifiche asincrone del database per risolvere la violazione.
Ad esempio, se due richieste tentano di eliminare la stessa entità, la seconda eliminazione ha esito negativo perché la riga non esiste più. Un intercettore può eliminare questo problema, poiché il risultato finale è lo stesso:
public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
public InterceptionResult ThrowingConcurrencyException(
ConcurrencyExceptionEventData eventData,
InterceptionResult result)
{
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
{
return InterceptionResult.Suppress();
}
return result;
}
}
Per altre informazioni ed esempi, vedere Intercettazione della concorrenza ottimistica.
Inizializzazione differita di una stringa di connessione
IDbConnectionInterceptor può essere usato per configurare dinamicamente le stringhe di connessione al momento in cui viene usata la connessione, ad esempio per supportare stringhe di connessione per tenant in un sistema multi-tenant. Il ConnectionOpeningAsync metodo può eseguire un'operazione asincrona per ottenere la stringa di connessione, trovare un token di accesso e così via.
Ad esempio, l'intercettore seguente imposta la stringa di connessione la prima volta che viene aperta la connessione:
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly ITenantConnectionStringFactory _connectionStringFactory;
public ConnectionStringInitializationInterceptor(ITenantConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}
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;
}
}
Per altre informazioni ed esempi, vedere Inizializzazione differita di una stringa di connessione.
Registrazione delle statistiche delle query di SQL Server
Due intercettori possono collaborare per inviare statistiche di query di SQL Server al log delle applicazioni. Un IDbCommandInterceptor comando prefisssi con SET STATISTICS IO ON e usa il nuovo DataReaderClosingAsync metodo per leggere i risultati delle statistiche. Un IDbConnectionInterceptor si aggancia al nuovo metodo ConnectionCreated per collegarsi all'evento SqlConnection.InfoMessage.
Ad esempio, l'intercettore di comandi prefigge la direttiva statistiche:
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);
}
Per altre informazioni ed esempi, vedere Registrazione delle statistiche di query di SQL Server.
Miglioramenti delle query
EF7 contiene molti miglioramenti nella traduzione delle query LINQ.
GroupBy come operatore finale
Suggerimento
Il codice illustrato di seguito proviene da GroupByFinalOperatorSample.cs.
EF7 supporta l'uso di GroupBy come operatore finale nelle query. Ad esempio, la query LINQ seguente:
var query = context.Books.GroupBy(s => s.Price);
Viene convertito nel codice SQL seguente quando si usa SQL Server:
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]
Nota
Questo tipo di GroupBy non si traduce direttamente in SQL, quindi EF Core esegue il raggruppamento sui risultati restituiti. Tuttavia, ciò non comporta il trasferimento di dati aggiuntivi dal server.
GroupJoin come operatore finale
Suggerimento
Il codice illustrato di seguito proviene da GroupJoinFinalOperatorSample.cs.
EF7 supporta l'uso di GroupJoin come operatore finale nelle query. Ad esempio, la query LINQ seguente:
var query = context.Customers.GroupJoin(
context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });
Viene convertito nel codice SQL seguente quando si usa 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]
Tipo di entità GroupBy
Suggerimento
Il codice illustrato di seguito proviene da GroupByEntityTypeSample.cs.
EF7 supporta il raggruppamento in base a un tipo di entità. Ad esempio, la query LINQ seguente:
var query = context.Books
.GroupBy(s => s.Author)
.Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });
Viene convertito nel codice SQL seguente quando si usa 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]
Tenere presente che il raggruppamento in base a una proprietà univoca, ad esempio la chiave primaria, sarà sempre più efficiente rispetto al raggruppamento in base a un tipo di entità. Tuttavia, il raggruppamento in base ai tipi di entità può essere usato sia per i tipi di entità con chiave che per i tipi di entità senza chiave.
Inoltre, il raggruppamento in base a un tipo di entità con una chiave primaria comporterà sempre un gruppo per ogni istanza di entità, poiché ogni entità deve avere un valore di chiave univoco. A volte vale la pena cambiare l'origine della query in modo che il raggruppamento non sia necessario. Ad esempio, la query seguente restituisce gli stessi risultati della query precedente:
var query = context.Authors
.Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });
Questa query viene convertita nel codice SQL seguente quando si usa 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]
Le sottoquery non fanno riferimento a colonne non raggruppate dalla query esterna
Suggerimento
Il codice illustrato di seguito proviene da UngroupedColumnsQuerySample.cs.
In EF Core 6.0, una GROUP BY clausola fa riferimento a colonne nella query esterna, che fallisce con alcuni database ed è inefficiente in altri. Ad esempio, si consideri la query seguente:
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)
};
In EF Core 6.0 in SQL Server questo è stato convertito in:
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])
In EF7 la traduzione è:
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]
Le raccolte di sola lettura possono essere usate per Contains
Suggerimento
Il codice illustrato di seguito proviene da ReadOnlySetQuerySample.cs.
EF7 supporta l'uso di Contains quando gli elementi da cercare sono contenuti in un IReadOnlySet oggetto o IReadOnlyCollectiono IReadOnlyList. Ad esempio, la query LINQ seguente:
IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));
Viene convertito nel codice SQL seguente quando si usa 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))
Traduzioni per le funzioni di aggregazione
EF7 introduce una migliore estendibilità per i provider per tradurre le funzioni di aggregazione. Questo e altri lavori in questo settore hanno portato a diverse nuove traduzioni tra provider, tra cui:
-
Traduzione di
String.JoineString.Concat - Traduzione di funzioni di aggregazione spaziale
- Traduzione delle funzioni di aggregazione delle statistiche
Nota
Le funzioni di aggregazione che agiscono sull'argomento IEnumerable vengono in genere convertite solo nelle GroupBy query. Vota per supportare i tipi spaziali nelle colonne JSON se sei interessato a rimuovere questa limitazione.
Funzioni di aggregazione di stringhe
Suggerimento
Il codice illustrato di seguito proviene da StringAggregateFunctionsSample.cs.
Le query che usano Join e Concat vengono ora convertite quando appropriato. Ad esempio:
var query = context.Posts
.GroupBy(post => post.Author)
.Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });
Questa query viene convertita nell'esempio seguente quando si usa 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]
In combinazione con altre funzioni stringa, queste traduzioni consentono una manipolazione complessa delle stringhe nel server. Ad esempio:
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) + "' "))
});
Questa query viene convertita nell'esempio seguente quando si usa 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]
Funzioni di aggregazione spaziale
Suggerimento
Il codice illustrato di seguito proviene da SpatialAggregateFunctionsSample.cs.
È ora possibile per i provider di database che supportano NetTopologySuite tradurre le seguenti funzioni di aggregazione spaziale:
- GeometryCombiner.Combine()
- UnaryUnionOp.Union()
- ConvexHull.Create()
- EnvelopeCombiner.CombineAsGeometry()
Suggerimento
Queste traduzioni sono state implementate dal team per SQL Server e SQLite. Per altri provider, contattare il gestore del provider per aggiungere il supporto se è stato implementato per tale provider.
Ad esempio:
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)) });
Questa query viene convertita nel codice SQL seguente quando si usa 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]
Funzioni di aggregazione statistiche
Suggerimento
Il codice illustrato di seguito proviene da StatisticalAggregateFunctionsSample.cs.
Le traduzioni di SQL Server sono state implementate per le funzioni statistiche seguenti:
Suggerimento
Queste traduzioni sono state implementate dal team per SQL Server. Per altri provider, contattare il gestore del provider per aggiungere il supporto se è stato implementato per tale provider.
Ad esempio:
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))
});
Questa query viene convertita nel codice SQL seguente quando si usa 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]
Traduzione di string.IndexOf
Suggerimento
Il codice illustrato di seguito proviene da MiscellaneousTranslationsSample.cs.
EF7 ora traduce String.IndexOf nelle query LINQ. Ad esempio:
var query = context.Posts
.Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
.Where(post => post.IndexOfEntity > 0);
Questa query viene convertita nel codice SQL seguente quando si usa 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
Traduzione di GetType per i tipi di entità
Suggerimento
Il codice illustrato di seguito proviene da MiscellaneousTranslationsSample.cs.
EF7 ora traduce Object.GetType() nelle query LINQ. Ad esempio:
var query = context.Posts.Where(post => post.GetType() == typeof(Post));
Questa query viene convertita nel codice SQL seguente quando si usa SQL Server con ereditarietà TPH:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'
Si noti che questa query restituisce solo Post le istanze di tipo Poste non quelle di qualsiasi tipo derivato. Questo comportamento è diverso da una query che usa is o OfType, che restituirà anche istanze di qualsiasi tipo derivato. Si consideri ad esempio la query:
var query = context.Posts.OfType<Post>();
Che si traduce in SQL diverso:
SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
E restituirà sia le entità Post che FeaturedPost.
Supporto per AT TIME ZONE
Suggerimento
Il codice illustrato di seguito proviene da MiscellaneousTranslationsSample.cs.
EF7 introduce nuove AtTimeZone funzioni per DateTime e DateTimeOffset. Queste funzioni si traducono in AT TIME ZONE clausole in SQL generato. Ad esempio:
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"),
});
Questa query viene convertita nel codice SQL seguente quando si usa 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]
Suggerimento
Queste traduzioni sono state implementate dal team per SQL Server. Per altri provider, contattare il gestore del provider per aggiungere il supporto se è stato implementato per tale provider.
Inclusione filtrata sulle navigazioni nascoste
Suggerimento
Il codice illustrato di seguito proviene da MiscellaneousTranslationsSample.cs.
I metodi Include possono ora essere usati con EF.Property. In questo modo è possibile filtrare e ordinare anche le proprietà di navigazione private o gli spostamenti privati rappresentati dai campi. Ad esempio:
var query = context.Blogs.Include(
blog => EF.Property<ICollection<Post>>(blog, "Posts")
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Equivale a:
var query = context.Blogs.Include(
blog => Posts
.Where(post => post.Content.Contains(".NET"))
.OrderBy(post => post.Title));
Ma non deve Blog.Posts essere accessibile pubblicamente.
Quando si usa SQL Server, entrambe le query precedenti vengono convertite in:
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]
Traduzione di Cosmos per Regex.IsMatch
Suggerimento
Il codice illustrato di seguito proviene da CosmosQueriesSample.cs.
EF7 supporta l'uso Regex.IsMatch nelle query LINQ su Azure Cosmos DB. Ad esempio:
var containsInnerT = await context.Triangles
.Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
.ToListAsync();
Esegue la conversione nel codice SQL seguente:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))
Miglioramenti apportati all'API DbContext e al comportamento
EF7 contiene diversi miglioramenti alle classi DbContext e ad altre classi correlate.
Suggerimento
Il codice per gli esempi in questa sezione proviene da DbContextApiSample.cs.
Suppressore per proprietà DbSet non inizializzate
Le proprietà pubbliche e impostabili DbSet in un DbContext vengono inizializzate automaticamente da EF Core durante la costruzione di DbContext. Si consideri ad esempio la definizione seguente DbContext :
public class SomeDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}
La Blogs proprietà verrà impostata su un'istanza DbSet<Blog> come parte della costruzione dell'istanza DbContext . In questo modo è possibile usare il contesto per le query senza passaggi aggiuntivi.
Tuttavia, con l'introduzione dei tipi di riferimento nullable in C#, il compilatore ora avvisa che la proprietà Blogs non nullable non è inizializzata.
[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Questo è un avviso fasullo; la proprietà viene impostata su un valore non nullo da EF Core. Inoltre, dichiarare la proprietà come nullabile farà sparire l'avviso, ma non è una buona idea perché, concettualmente, la proprietà non è nullabile e non sarà mai nulla.
EF7 contiene un DiagnosticSuppressor per DbSet le proprietà di un oggetto DbContext che impedisce al compilatore di generare questo avviso.
Suggerimento
Questo modello ha avuto origine nei giorni in cui le proprietà automatiche C# erano molto limitate. Con C# moderno, è consigliabile impostare le proprietà automatiche di sola lettura e quindi inizializzarle in modo esplicito nel DbContext costruttore oppure ottenere l'istanza memorizzata DbSet nella cache dal contesto quando necessario. Ad esempio: public DbSet<Blog> Blogs => Set<Blog>().
Distinguere l'annullamento dagli errori nei log
In alcuni casi un'applicazione annulla in modo esplicito una query o un'altra operazione di database. Questa operazione viene in genere eseguita usando un CancellationToken passato al metodo che esegue l'operazione.
In EF Core 6, gli eventi registrati quando un'operazione viene annullata sono uguali a quelli registrati quando l'operazione non riesce per altri motivi. EF7 introduce nuovi eventi di log specifici per le operazioni di database annullate. Questi nuovi eventi sono, per impostazione predefinita, registrati a Debug livello. La tabella seguente illustra gli eventi pertinenti e i relativi livelli di log predefiniti:
| Evento | Descrizione | Livello di log predefinito |
|---|---|---|
| CoreEventId.QueryIterationFailed | Errore durante l'elaborazione dei risultati di una query. | LogLevel.Error |
| CoreEventId.SaveChangesFailed | Si è verificato un errore durante il tentativo di salvare le modifiche nel database. | LogLevel.Error |
| RelationalEventId.CommandError | Si è verificato un errore durante l'esecuzione di un comando di database. | LogLevel.Error |
| CoreEventId.QueryCanceled | Una query è stata annullata. | LogLevel.Debug |
| CoreEventId.SaveChangesCanceled | Il comando del database è stato annullato durante il tentativo di salvare le modifiche. | LogLevel.Debug |
| RelationalEventId.CommandCanceled | L'esecuzione di un oggetto DbCommand è stata annullata. |
LogLevel.Debug |
Nota
L'annullamento viene rilevato esaminando l'eccezione anziché controllare il token di annullamento. Ciò significa che gli annullamenti non attivati tramite il token di annullamento verranno comunque rilevati e registrati in questo modo.
Nuovi IProperty e INavigation sovraccarichi per i metodi EntityEntry
Il codice che lavora con il modello di Entity Framework include spesso un IProperty o INavigation che rappresenta i metadati della proprietà o della navigazione. Un EntityEntry viene quindi usato per ottenere il valore di proprietà/navigazione o eseguire una query sul relativo stato. Tuttavia, prima di EF7, questo richiedeva il passaggio del nome della proprietà o della relazione di navigazione ai metodi di EntityEntry, che poi rieseguivano l'operazione di ricerca di IProperty o INavigation. In EF7 l'oggetto IProperty o INavigation può invece essere passato direttamente, evitando la ricerca aggiuntiva.
Si consideri ad esempio un metodo per trovare tutti i fratelli di una determinata entità:
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));
}
Questo metodo trova l'elemento padre di una determinata entità e quindi passa l'inverso INavigation al Collection metodo della voce padre. Questi metadati vengono quindi usati per restituire tutti i fratelli dell'elemento padre specificato. Di seguito è riportato un esempio dell'uso:
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}'");
}
E l'output:
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 per i tipi di entità di tipo condiviso
EF Core può usare lo stesso tipo CLR per più tipi di entità diversi. Questi tipi sono noti come "tipi di entità di tipo condiviso" e vengono spesso usati per eseguire il mapping di un tipo di dizionario con coppie chiave/valore usate per le proprietà del tipo di entità. Ad esempio, è possibile definire un BuildMetadata tipo di entità senza definire un tipo CLR dedicato:
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");
});
Si noti che il tipo di entità di tipo condiviso deve essere denominato. In questo caso, il nome è BuildMetadata. Questi tipi di entità sono quindi accessibili tramite un DbSet per il tipo di entità che viene ottenuto utilizzando il nome. Ad esempio:
public DbSet<Dictionary<string, object>> BuildMetadata
=> Set<Dictionary<string, object>>("BuildMetadata");
Questo DbSet può essere utilizzato per monitorare le istanze di entità:
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" }
});
Eseguire interrogazioni:
var builds = await context.BuildMetadata
.Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
.OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
.ToListAsync();
In EF7 è ora disponibile anche un Entry metodo su DbSet che è possibile usare per ottenere lo stato di un'istanza, anche se non è ancora tracciato. Ad esempio:
var state = context.BuildMetadata.Entry(build).State;
ContextInitialized viene ora registrato come Debug
In EF7 l'evento ContextInitialized viene registrato a Debug livello. Ad esempio:
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
Nelle versioni precedenti, è stato registrato al livello Information. Ad esempio:
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
Se lo si desidera, il livello di log può essere nuovamente modificato in Information:
optionsBuilder.ConfigureWarnings(
builder =>
{
builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
});
IEntityEntryGraphIterator è utilizzabile pubblicamente
In EF7 il IEntityEntryGraphIterator servizio può essere usato dalle applicazioni. Si tratta del servizio usato internamente quando si individua un grafico di entità da monitorare, e anche da parte di TrackGraph. Ecco un esempio che itera su tutte le entità raggiungibili da un'entità iniziale.
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();
Avviso:
- L'iteratore interrompe l'attraversamento da un determinato nodo quando il delegato di callback restituisce
false. Questo esempio tiene traccia delle entità visitate e restituiscefalsequando l'entità è già stata visitata. Ciò impedisce cicli infiniti risultanti da cicli nel grafico. - L'oggetto
EntityEntryGraphNode<TState>consente di passare lo stato senza includerlo nel delegato. - Per ogni nodo visitato diverso dal primo, il nodo da cui è stato scoperto e il percorso attraverso cui è stato scoperto vengono passati al callback.
Miglioramenti alla creazione di modelli
EF7 contiene un'ampia gamma di piccoli miglioramenti nella creazione di modelli.
Suggerimento
Il codice per gli esempi in questa sezione proviene da ModelBuildingSample.cs.
Gli indici possono essere crescente o decrescente
Per impostazione predefinita, EF Core crea indici ascendenti. EF7 supporta anche la creazione di indici decrescente. Ad esempio:
modelBuilder
.Entity<Post>()
.HasIndex(post => post.Title)
.IsDescending();
In alternativa, usando l'attributo Index di mapping:
[Index(nameof(Title), AllDescending = true)]
public class Post
{
public int Id { get; set; }
[MaxLength(64)]
public string? Title { get; set; }
}
Questa operazione è raramente utile per gli indici su una singola colonna, poiché il database può usare lo stesso indice per l'ordinamento in entrambe le direzioni. Tuttavia, questo non è il caso per gli indici compositi su più colonne in cui l'ordine in ogni colonna può essere importante. EF Core supporta questa funzionalità consentendo a più colonne di avere un ordinamento diverso definito per ogni colonna. Ad esempio:
modelBuilder
.Entity<Blog>()
.HasIndex(blog => new { blog.Name, blog.Owner })
.IsDescending(false, true);
In alternativa, usando un attributo di mapping:
[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();
}
Questo risultato è il codice SQL seguente quando si usa SQL Server:
CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);
Infine, è possibile creare più indici nello stesso set ordinato di colonne assegnando i nomi degli indici. Ad esempio:
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);
In alternativa, usando gli attributi di mapping:
[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();
}
In questo modo viene generato il codice SQL seguente in 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);
Attributo di mapping per chiavi composite
EF7 introduce un nuovo attributo di mapping (noto anche come "annotazione dati") per specificare la proprietà o le proprietà della chiave primaria di qualsiasi tipo di entità. A differenza di System.ComponentModel.DataAnnotations.KeyAttribute, PrimaryKeyAttribute viene inserito nella classe del tipo di entità anziché nella proprietà della chiave. Ad esempio:
[PrimaryKey(nameof(PostKey))]
public class Post
{
public int PostKey { get; set; }
}
In questo modo è possibile definire chiavi composite in modo naturale:
[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
public int PostId { get; set; }
public int CommentId { get; set; }
public string CommentText { get; set; } = null!;
}
La definizione dell'indice nella classe significa anche che può essere usata per specificare proprietà o campi privati come chiavi, anche se in genere vengono ignorati durante la compilazione del modello di Entity Framework. Ad esempio:
[PrimaryKey(nameof(_id))]
public class Tag
{
private readonly int _id;
}
DeleteBehavior attributo di mapping
EF7 introduce un attributo di mapping (noto anche come "annotazione dati") per specificare una relazione per DeleteBehavior. Ad esempio, le relazioni obbligatorie vengono create con DeleteBehavior.Cascade per impostazione predefinita. Questa opzione può essere modificata a DeleteBehavior.NoAction per impostazione predefinita usando DeleteBehaviorAttribute:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
[DeleteBehavior(DeleteBehavior.NoAction)]
public Blog Blog { get; set; } = null!;
}
In questo modo verranno disabilitate le eliminazioni a catena per la relazione Blog-Post.
Proprietà mappate a nomi di colonna diversi
Alcuni modelli di mapping comportano il mapping della stessa proprietà CLR a una colonna in ognuna di più tabelle diverse. EF7 consente a queste colonne di avere nomi diversi. Si consideri, ad esempio, una semplice gerarchia di ereditarietà:
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; }
}
Con la strategia di mapping dell'ereditarietà TPT, questi tipi verranno mappati a tre tabelle. Tuttavia, la colonna chiave primaria in ogni tabella può avere un nome diverso. Ad esempio:
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
);
EF7 consente di configurare questo mapping usando un generatore di tabelle annidato:
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"));
Utilizzando il mapping dell'ereditarietà TPC, la proprietà Breed può anche essere associata a nomi delle colonne diversi in tabelle diverse. Si considerino ad esempio le tabelle TPC seguenti:
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 supporta la mappatura delle tabelle.
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");
});
Relazioni molti-a-molti unidirezionali
EF7 supporta relazioni di tipo molti-a-molti in cui uno dei due lati non dispone di una proprietà di navigazione. Si considerino, ad esempio, i tipi Post e Tag:
public class Post
{
public int Id { get; set; }
public string? Title { get; set; }
public Blog Blog { get; set; } = null!;
public List<Tag> Tags { get; } = new();
}
public class Tag
{
public int Id { get; set; }
public string TagName { get; set; } = null!;
}
Si noti che il Post tipo ha una proprietà di navigazione per un elenco di tag, ma il Tag tipo non dispone di una proprietà di navigazione per i post. In EF7, questo può comunque essere configurato come relazione molti-a-molti, consentendo l'uso dello stesso Tag oggetto per molti post diversi. Ad esempio:
modelBuilder
.Entity<Post>()
.HasMany(post => post.Tags)
.WithMany();
Questo comporta l'associazione alla tabella di join appropriata.
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
);
E la relazione può essere usata come molti-a-molti nel modo normale. Ad esempio, inserendo alcuni post che condividono vari tag da un set comune:
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();
Suddivisione di entità
La suddivisione delle entità mappa un singolo tipo di entità su più tabelle. Si consideri, ad esempio, un database con tre tabelle che contengono i dati dei clienti:
- Tabella
Customersper le informazioni sui clienti - Tabella
PhoneNumbersper il numero di telefono del cliente - Tabella
Addressesper l'indirizzo del cliente
Ecco le definizioni per queste tabelle in 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
);
Ognuna di queste tabelle viene in genere mappata al proprio tipo di entità, con relazioni tra i tipi. Tuttavia, se tutte e tre le tabelle vengono sempre usate insieme, può essere più conveniente eseguirne il mapping a un singolo tipo di entità. Ad esempio:
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; }
}
Questo risultato viene ottenuto in EF7 chiamando SplitToTable per ogni divisione nel tipo di entità. Ad esempio, il codice seguente suddivide il Customer tipo di entità nelle Customerstabelle , PhoneNumberse Addresses illustrate in precedenza:
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);
});
});
Si noti anche che, se necessario, è possibile specificare nomi di colonna chiave primaria diversi per ognuna delle tabelle.
Stringhe UTF-8 di SQL Server
Le stringhe Unicode di SQL Server rappresentate dai tipi ncharnvarchar vengono archiviate come UTF-16. Inoltre, i char tipi di dati e varchar vengono usati per archiviare stringhe non Unicode con supporto per vari set di caratteri.
A partire da SQL Server 2019, i char tipi di dati e varchar possono essere usati per archiviare invece stringhe Unicode con codifica UTF-8 . L'oggetto viene ottenuto impostando una delle collazioni UTF-8. Ad esempio, il codice seguente configura una stringa UTF-8 di SQL Server a lunghezza variabile per la CommentText colonna:
modelBuilder
.Entity<Comment>()
.Property(comment => comment.CommentText)
.HasColumnType("varchar(max)")
.UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");
Questa configurazione genera la definizione di colonna di SQL Server seguente:
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])
);
Le tabelle temporali supportano entità di proprietà
Il mapping delle tabelle temporali di SQL Server di EF Core è stato migliorato in EF7 per supportare la condivisione delle tabelle. In particolare, il mapping predefinito per entità singole di proprietà usa condivisione di tabelle.
Si consideri, ad esempio, un tipo di entità proprietaria Employee e il tipo di entità posseduta 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; }
}
Se questi tipi vengono mappati alla stessa tabella, in EF7 è possibile creare una tabella temporale:
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");
}));
Nota
Per semplificare questa configurazione, è possibile tenere traccia del problema n. 29303. Vota per questa questione se desideri vederla implementata.
Generazione di valori migliorata
EF7 include due miglioramenti significativi alla generazione automatica dei valori per le proprietà chiave.
Suggerimento
Il codice per gli esempi in questa sezione proviene da ValueGenerationSample.cs.
Generazione di valori per i tipi protetti DDD
Nella progettazione basata su dominio (DDD), le "chiavi sorvegliate" possono migliorare la sicurezza dei tipi delle proprietà delle chiavi. Ciò si ottiene incapsulando il tipo di chiave in un altro tipo appositamente definito per l'uso della chiave. Ad esempio, il codice seguente definisce un ProductId tipo per i codici Product Key e un CategoryId tipo per le chiavi di categoria.
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; }
}
Vengono quindi utilizzati nei tipi di entità Product e Category.
public class Product
{
public Product(string name) => Name = name;
public ProductId Id { get; set; }
public string Name { get; set; }
public CategoryId CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
public class Category
{
public Category(string name) => Name = name;
public CategoryId Id { get; set; }
public string Name { get; set; }
public List<Product> Products { get; } = new();
}
Ciò rende impossibile assegnare accidentalmente l'ID per una categoria a un prodotto o viceversa.
Avviso
Come per molti concetti DDD, questa maggiore sicurezza dei tipi è a scapito di una maggiore complessità del codice. Vale la pena considerare se, ad esempio, l'assegnazione di un ID prodotto a una categoria è qualcosa che potrebbe accadere. Mantenere le cose semplici può essere complessivamente più vantaggioso per la codebase.
I tipi di chiave protetti illustrati di seguito avvolgono entrambi i valori chiave int, il che significa che i valori interi verranno utilizzati nelle tabelle di database mappate. Questa operazione viene ottenuta definendo convertitori di valori per i tipi:
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))
{
}
}
Nota
Il codice usa tipi struct qui. Ciò significa che hanno una semantica di tipo valore appropriata da usare come chiavi. Se vengono usati tipi class invece, è necessario eseguire l'override della semantica di uguaglianza o specificare anche un comparatore di valori.
In EF7 i tipi di chiave basati sui convertitori di valori possono usare valori di chiave generati automaticamente, purché il tipo sottostante supporti questa opzione. Questa configurazione viene configurata nel modo normale usando ValueGeneratedOnAdd:
modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();
Per impostazione predefinita, si ottengono colonne IDENTITY quando usate con 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);
Che vengono usati nel modo normale per generare valori chiave durante l'inserimento di entità:
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;
Generazione di chiavi basate su sequenza per SQL Server
EF Core supporta la generazione di valori chiave usando colonne di IDENTITY o uno schema Hi-Lo basato su blocchi di chiavi generate da una sequenza di database. EF7 introduce il supporto per una sequenza di database collegata al vincolo predefinito della colonna della chiave. Nel suo formato più semplice, è sufficiente indicare a EF Core di usare una sequenza per la proprietà chiave:
modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();
Ciò comporta la definizione di una sequenza nel database:
CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;
Che viene quindi usato nel vincolo predefinito della colonna chiave:
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);
Nota
Questa forma di generazione di chiavi viene usata per impostazione predefinita per le chiavi generate nelle gerarchie dei tipi di entità usando la strategia di mapping TPC.
Se necessario, alla sequenza può essere assegnato un nome e uno schema diversi. Ad esempio:
modelBuilder
.Entity<Product>()
.Property(product => product.Id)
.UseSequence("ProductsSequence", "northwind");
Un'ulteriore configurazione della sequenza viene formata configurandola in modo esplicito nel modello. Ad esempio:
modelBuilder
.HasSequence<int>("ProductsSequence", "northwind")
.StartsAt(1000)
.IncrementsBy(2);
Miglioramenti agli strumenti per le migrazioni
EF7 include due miglioramenti significativi quando si usano gli strumenti da riga di comando di EF Core Migrations.
UseSqlServer e simili accettano null
È molto comune leggere un stringa di connessione da un file di configurazione e quindi passare tale stringa di connessione a UseSqlServer, UseSqliteo il metodo equivalente per un altro provider. Ad esempio:
services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));
È anche comune passare una stringa di connessione quando si applicano migrazioni. Ad esempio:
dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
In alternativa, quando si usa un Migrations bundle.
./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"
In questo caso, anche se la stringa di connessione letta dalla configurazione non viene usata, il codice di avvio dell'applicazione tenta comunque di leggerlo dalla configurazione e passarlo a UseSqlServer. Se la configurazione non è disponibile, questo comporta il passaggio di null a UseSqlServer. In EF7, ciò è consentito, purché la stringa di connessione venga impostata successivamente, ad esempio tramite --connection allo strumento da riga di comando.
Nota
Questa modifica è stata apportata per UseSqlServer e UseSqlite. Per altri provider, contattare il gestore del provider per apportare una modifica equivalente se non è ancora stata eseguita per tale provider.
Rilevare quando gli strumenti sono in esecuzione
EF Core esegue il codice dell'applicazione quando vengono usati i dotnet-ef comandi di o PowerShell . A volte può essere necessario rilevare questa situazione per impedire l'esecuzione di codice inappropriato in fase di progettazione. Ad esempio, il codice che applica automaticamente le migrazioni all'avvio dovrebbe probabilmente non eseguire questa operazione in fase di progettazione. In EF7 è possibile rilevare questo valore usando il EF.IsDesignTime flag :
if (!EF.IsDesignTime)
{
await context.Database.MigrateAsync();
}
EF Core imposta IsDesignTime su true quando il codice dell'applicazione è in esecuzione per gli strumenti.
Miglioramenti delle prestazioni per i proxy
EF Core supporta proxy generati dinamicamente per il caricamento lazy e il rilevamento delle modifiche. EF7 contiene due miglioramenti delle prestazioni quando si usano questi proxy:
- I tipi di proxy ora vengono creati con pigrizia. Ciò significa che il tempo di compilazione iniziale del modello quando si usano proxy può essere molto più veloce con EF7 rispetto a EF Core 6.0.
- I proxy possono ora essere usati con i modelli compilati.
Ecco alcuni risultati delle prestazioni per un modello con 449 tipi di entità, 6390 proprietà e 720 relazioni.
| Sceneggiatura | metodo | Media | Errore | StdDev |
|---|---|---|---|---|
| EF Core 6.0 senza proxy | TimeToFirstQuery | 1,085 s | 0,0083 s | 0.0167 s |
| EF Core 6.0 con proxy di rilevamento delle modifiche | TimeToFirstQuery | 13.01 s | 0.2040 s | 0.4110 s |
| EF Core 7.0 senza proxy | TimeToFirstQuery | 1.442 s | 0.0134 s | 0.0272 s |
| EF Core 7.0 con proxy di rilevamento delle modifiche | TimeToFirstQuery | 1.446 s | 0.0160 s | 0.0323 s |
| EF Core 7.0 con proxy di rilevamento delle modifiche e modello compilato | TimeToFirstQuery | 0.162 s | 0.0062 s | 0.0125 s |
In questo caso, quindi, un modello con proxy di rilevamento delle modifiche può essere pronto per eseguire la prima query 80 volte più veloce in EF7 rispetto a quanto possibile con EF Core 6.0.
Data binding eccellente di Windows Forms
Il team Windows Form ha apportato alcuni importanti miglioramenti all'esperienza di Progettazione di Visual Studio. Sono incluse nuove esperienze per il data binding che si integra bene con EF Core.
In breve, la nuova esperienza offre Visual Studio U.I. per la creazione di un ObjectDataSourceoggetto :
Questo può quindi essere associato a un'istanza di EF Core DbSet con codice semplice:
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;
}
}
Vedere Introduzione alle Windows Form per una procedura dettagliata completa e un'applicazione di esempio WinForms scaricabile.