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.
Panoramica delle chiavi esterne e degli spostamenti
Le relazioni in un modello Entity Framework Core (EF Core) vengono rappresentate usando chiavi esterne (FK). Un FK è costituito da una o più proprietà dell'entità dipendente o figlio nella relazione. Questa entità dipendente/figlio è associata a una determinata entità principale/padre quando i valori delle proprietà della chiave esterna sul dipendente/figlio corrispondono ai valori delle proprietà chiave alternativa o primaria (PK) nell'entità principale/padre.
Le chiavi esterne sono un buon modo per archiviare e modificare le relazioni nel database, ma non sono molto semplici quando si usano più entità correlate nel codice dell'applicazione. Pertanto, la maggior parte dei modelli di EF Core integra anche le navigazioni sulla rappresentazione FK. Le navigazioni sono riferimenti C#/.NET tra istanze di entità che riflettono le associazioni trovate attraverso la corrispondenza dei valori di chiave esterna con i valori di chiave primaria o alternativa.
Le navigazioni possono essere usate su entrambi i lati della relazione, su un solo lato o non affatto, lasciando solo la proprietà FK. La proprietà FK può essere nascosta rendendola una proprietà ombra. Per altre informazioni sulla modellazione delle relazioni, vedere Relazioni .
Suggerimento
Questo documento presuppone che gli stati dell'entità e le nozioni di base del rilevamento delle modifiche di EF Core siano compresi. Per altre informazioni su questi argomenti, vedere Rilevamento modifiche in EF Core .
Suggerimento
È possibile eseguire ed eseguire il debug in tutto il codice di questo documento scaricando il codice di esempio da GitHub.
Modello di esempio
Il modello seguente contiene quattro tipi di entità con relazioni tra di esse. I commenti nel codice indicano quali proprietà sono chiavi esterne, chiavi primarie e navigazioni.
public class Blog
{
public int Id { get; set; } // Primary key
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
public BlogAssets Assets { get; set; } // Reference navigation
}
public class BlogAssets
{
public int Id { get; set; } // Primary key
public byte[] Banner { get; set; }
public int? BlogId { get; set; } // Foreign key
public Blog Blog { get; set; } // Reference navigation
}
public class Post
{
public int Id { get; set; } // Primary key
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; } // Foreign key
public Blog Blog { get; set; } // Reference navigation
public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}
public class Tag
{
public int Id { get; set; } // Primary key
public string Text { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}
Le tre relazioni in questo modello sono:
- Ogni blog può avere molti post (uno-a-molti):
-
Blog
è l'entità principale/genitore. -
Post
è il dipendente/figlio. Contiene la proprietàPost.BlogId
FK , il cui valore deve corrispondere alBlog.Id
valore PK del blog correlato. -
Post.Blog
è una navigazione di riferimento da un post al blog associato.Post.Blog
è la navigazione inversa perBlog.Posts
. -
Blog.Posts
è una navigazione raccolta da un blog a tutti i post associati.Blog.Posts
è la navigazione inversa perPost.Blog
.
-
- Ogni blog può avere uno asset (uno-a-uno):
-
Blog
è l'entità principale/genitore. -
BlogAssets
è il dipendente/figlio. Contiene la proprietàBlogAssets.BlogId
FK , il cui valore deve corrispondere alBlog.Id
valore PK del blog correlato. -
BlogAssets.Blog
è una navigazione di riferimento dalle risorse al blog associato.BlogAssets.Blog
è la navigazione inversa perBlog.Assets
. -
Blog.Assets
è una navigazione di riferimento dal blog alle risorse associate.Blog.Assets
è la navigazione inversa perBlogAssets.Blog
.
-
- Ogni post può avere molti tag e ogni tag può avere molti post (molti a molti):
- Le relazioni molti-a-molti costituiscono un ulteriore strato rispetto a due relazioni uno-a-molti. Le relazioni molti-a-molti sono descritte più avanti in questo documento.
-
Post.Tags
è una navigazione di raccolta da un post a tutti i tag associati.Post.Tags
è la navigazione inversa perTag.Posts
. -
Tag.Posts
è una navigazione di raccolta da un tag a tutti i post associati.Tag.Posts
è la navigazione inversa perPost.Tags
.
Per altre informazioni su come modellare e configurare le relazioni, vedere Relazioni .
Correzione delle relazioni
EF Core mantiene gli spostamenti in allineamento con i valori di chiave esterna e viceversa. Ovvero, se un valore di chiave esterna cambia in modo che ora faccia riferimento a un'entità principale/padre diversa, le navigazione vengono aggiornate in modo da riflettere questa modifica. Analogamente, se viene modificata una navigazione, i valori di chiave esterna delle entità coinvolte vengono aggiornati a riflettere il cambiamento. Questa operazione è denominata "sistemazione delle relazioni".
Modifica tramite query
La correzione si verifica prima quando vengono eseguite query sulle entità dal database. Il database ha solo valori di chiave esterna, quindi quando EF Core crea un'istanza di entità dal database usa i valori di chiave esterna per impostare gli spostamenti di riferimento e aggiungere entità agli spostamenti della raccolta in base alle esigenze. Si consideri, ad esempio, un'interrogazione per i blog e i relativi post e risorse associati.
using var context = new BlogsContext();
var blogs = await context.Blogs
.Include(e => e.Posts)
.Include(e => e.Assets)
.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Per ogni blog, EF Core creerà prima di tutto un'istanza Blog
. Quindi, quando ogni post viene caricato dal database, la navigazione Post.Blog
di riferimento viene impostata in modo che punti al blog associato. Analogamente, il post viene aggiunto alla navigazione della Blog.Posts
raccolta. La stessa cosa accade con BlogAssets
, tranne che in questo caso entrambi gli spostamenti sono riferimenti. La navigazione Blog.Assets
è impostata per puntare all'istanza degli asset, e la navigazione BlogAsserts.Blog
è impostata per puntare all'istanza del blog.
Esaminando la visualizzazione debug di rilevamento modifiche dopo questa query si vedono due blog, ciascuno con un asset e due post monitorati.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
La visualizzazione di debug mostra sia i valori chiave che gli spostamenti. Le navigazioni vengono visualizzate usando i valori delle chiavi primarie delle entità correlate. Nell'output sopra, ad esempio, Posts: [{Id: 1}, {Id: 2}]
indica che la navigazione della raccolta Blog.Posts
contiene due post correlati con chiavi primarie 1 e 2, rispettivamente. Analogamente, per ogni post associato al primo blog, la Blog: {Id: 1}
riga indica che la Post.Blog
navigazione fa riferimento al blog con chiave primaria 1.
Correzione alle entità tracciate localmente
La sistemazione delle relazioni si verifica anche tra le entità restituite da una query di tracciamento e le entità già tracciate dal DbContext. Si consideri ad esempio l'esecuzione di tre query separate per blog, post e asset:
using var context = new BlogsContext();
var blogs = await context.Blogs.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var assets = await context.Assets.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var posts = await context.Posts.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Esaminando di nuovo le viste di debug, dopo la prima query vengono rilevati solo i due blog:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: []
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: <null>
Posts: []
Le Blog.Assets
navigazioni di riferimento sono null e le navigazioni della Blog.Posts
raccolta sono vuote perché il contesto non sta attualmente tracciando entità associate.
Dopo la seconda query, le navigazioni di riferimento Blogs.Assets
sono state risolte in modo da puntare alle istanze BlogAsset
appena rilevate. Analogamente, gli BlogAssets.Blog
spostamenti di riferimento vengono impostati in modo che puntino all'istanza già rilevata Blog
appropriata.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: []
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: []
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Infine, dopo la terza query, le navigazioni della Blog.Posts
raccolta ora contengono tutti i post correlati, e i riferimenti Post.Blog
puntano all'istanza appropriata Blog
.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: 1}
Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Si tratta dello stesso stato finale ottenuto con la singola query originale, poiché EF Core ha sistemato le navigazioni mentre le entità venivano tracciate, anche quando provenivano da più query diverse.
Annotazioni
La correzione non comporta mai la restituzione di più dati dal database. Connette solo le entità già restituite dalla query o già rilevate da DbContext. Per informazioni sulla gestione dei duplicati durante la serializzazione delle entità, vedere Risoluzione delle identità in EF Core .
Modifica delle relazioni tramite le navigazioni
Il modo più semplice per modificare la relazione tra due entità consiste nel manipolare una navigazione, lasciando che EF Core corregga in modo appropriato la navigazione inversa e i valori FK. A tale scopo, è possibile:
- Aggiunta o rimozione di un'entità da una navigazione di una raccolta.
- Modifica di una navigazione di riferimento in modo che punti a un'entità diversa o impostarla su Null.
Aggiungere o rimuovere dalle navigazioni della raccolta
Ad esempio, spostare uno dei post dal blog di Visual Studio al blog di .NET. Ciò richiede prima di tutto il caricamento dei blog e dei post e quindi lo spostamento del post dalla raccolta di navigazione in un blog alla raccolta di navigazione nell'altro blog:
using var context = new BlogsContext();
var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == "Visual Studio Blog");
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Suggerimento
In questo caso è necessaria una chiamata a ChangeTracker.DetectChanges() perché l'accesso alla visualizzazione di debug non causa il rilevamento automatico delle modifiche.
Questa è la visualizzazione di debug stampata dopo l'esecuzione del codice precedente:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: <null>
Posts: [{Id: 4}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Tags: []
Post {Id: 3} Modified
Id: 3 PK
BlogId: 1 FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 1}
Tags: []
Post {Id: 4} Unchanged
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
La Blog.Posts
navigazione nel blog .NET ora contiene tre post (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
). Analogamente, la Blog.Posts
navigazione nel blog di Visual Studio include un solo post (Posts: [{Id: 4}]
). Questa operazione deve essere prevista perché il codice ha modificato in modo esplicito queste raccolte.
Più interessante, anche se il codice non ha modificato in modo esplicito la Post.Blog
navigazione, è stato risolto per puntare al blog di Visual Studio (Blog: {Id: 1}
). Inoltre, il Post.BlogId
valore della chiave esterna è stato aggiornato in modo che corrisponda al valore della chiave primaria del blog .NET. Questa modifica al valore FK in viene quindi salvata in modo permanente nel database quando viene chiamato SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
Modifica delle navigazioni di riferimento
Nell'esempio precedente, un post è stato spostato da un blog a un altro modificando lo spostamento della raccolta di post in ogni blog. La stessa cosa può essere ottenuta modificando invece la Post.Blog
navigazione di riferimento in modo che punti al nuovo blog. Per esempio:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;
La visualizzazione di debug dopo questa modifica è esattamente la stessa dell'esempio precedente. Questo perché EF Core ha rilevato la modifica della navigazione di riferimento e quindi corretto gli spostamenti della raccolta e il valore FK in modo che corrispondano.
Modifica delle relazioni tramite valori di chiave esterna
Nella sezione precedente le relazioni sono state manipolate dalle navigazioni, provocando l'aggiornamento automatico dei valori di chiave esterna. Questo è il modo consigliato per modificare le relazioni in EF Core. Tuttavia, è anche possibile modificare direttamente i valori FK. Ad esempio, è possibile spostare un post da un blog a un altro modificando il valore della Post.BlogId
chiave esterna:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;
Si noti che questo comportamento è molto simile alla modifica della navigazione di riferimento, come illustrato nell'esempio precedente.
La visualizzazione di debug dopo questa modifica è di nuovo identica a quella dei due esempi precedenti. Questo è dovuto al fatto che EF Core ha rilevato la modifica del valore FK e quindi corretto sia gli spostamenti di riferimento che di raccolta in modo che corrispondano.This is because EF Core detect the FK value change and then fixed up both the reference and collection navigations to match.
Suggerimento
Non scrivere codice per modificare tutti gli spostamenti e i valori FK ogni volta che una relazione cambia. Questo codice è più complesso e deve garantire modifiche coerenti a chiavi esterne e navigazioni in tutti i casi. Se possibile, gestisci una singola navigazione, oppure entrambe le navigazioni. Se necessario, è sufficiente modificare i valori FK. Evitate di manipolare sia le navigazioni che i valori FK.
Correzione per le entità aggiunte o eliminate
Aggiunta alla navigazione delle raccolte
EF Core esegue le azioni seguenti quando rileva che una nuova entità dipendente/figlio è stata aggiunta a una navigazione raccolta:
- Se l'entità non è tracciata, allora è tracciata. L'entità è in genere nello
Added
stato . Tuttavia, se il tipo di entità è configurato per l'uso di chiavi generate e viene impostato il valore della chiave primaria, l'entità viene rilevata nelloUnchanged
stato. - Se l'entità è associata a un'entità principale/padre diversa, tale relazione viene interrotta.
- L'entità diventa associata all'entità principale/genitore proprietaria della navigazione della raccolta.
- Le navigazioni e i valori di chiave esterna vengono sistemati per tutte le entità coinvolte.
In base a questo è possibile vedere che per spostare un post da un blog a un altro non è effettivamente necessario rimuoverlo dalla struttura di spostamento della raccolta precedente prima di aggiungerlo al nuovo. Il codice dell'esempio precedente può quindi essere modificato da:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);
A:
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);
EF Core rileva che il post è stato aggiunto a un nuovo blog e lo rimuove automaticamente dalla raccolta nel primo blog.
Rimozione dalla navigazione della raccolta
La rimozione di un'entità dipendente/figlio dalla navigazione nella raccolta dell'entità o dell'elemento padre provoca la separazione della relazione con tale entità/padre. L'operazione successiva dipende dal fatto che la relazione sia facoltativa o obbligatoria.
Relazioni facoltative
Per impostazione predefinita per le relazioni facoltative, il valore della chiave esterna è impostato su Null. Ciò significa che il dipendente/figlio non è più associato a alcun principale/genitore. Ad esempio, carichiamo un blog e i suoi post e poi rimuoviamo uno dei post dalla navigazione della raccolta Blog.Posts
.
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
Esaminando la vista debug del tracciamento delle modifiche dopo questa modifica mostra che:
- L'FK
Post.BlogId
è stato impostato su Null (BlogId: <null> FK Modified Originally 1
) - La
Post.Blog
navigazione di riferimento è stata impostata su Null (Blog: <null>
) - Il post è stato rimosso dalla navigazione della raccolta (
Blog.Posts
) (Posts: [{Id: 1}]
)
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Modified
Id: 2 PK
BlogId: <null> FK Modified Originally 1
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Tags: []
Si noti che il post non è contrassegnato come Deleted
. Viene contrassegnato come Modified
in modo che il valore FK nel database venga impostato su null quando viene chiamato SaveChanges.
Relazioni obbligatorie
L'impostazione del valore FK su null non è consentita (e in genere non è possibile) per le relazioni necessarie. Di conseguenza, interrompere una relazione obbligatoria significa che l'entità dipendente/figlio deve essere ri-assegnata a un nuovo principale/padre, o rimossa dal database quando viene eseguito il comando SaveChanges, per evitare una violazione del vincolo referenziale. Questo comportamento è noto come "eliminazione di orfani" ed è il comportamento predefinito in EF Core per le relazioni necessarie.
Si supponga, ad esempio, di modificare la relazione tra blog e post in modo che sia necessario e quindi eseguire lo stesso codice dell'esempio precedente:
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
Esaminando la visualizzazione di debug dopo questa modifica, viene mostrato quanto segue:
- Il post è stato contrassegnato come
Deleted
tale che verrà eliminato dal database quando viene chiamato SaveChanges. - La
Post.Blog
navigazione di riferimento è stata impostata su null (Blog: <null>
). - Il post è stato rimosso dalla navigazione della raccolta
Blog.Posts
(Posts: [{Id: 1}]
).
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: <null>
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Tags: []
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Tags: []
Si noti che l'oggetto Post.BlogId
rimane invariato perché per una relazione obbligatoria non può essere impostato su Null.
La chiamata a SaveChanges comporta l'eliminazione del post orfano:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Eliminare i tempi orfani e ri-padre
Per impostazione predefinita, contrassegnare gli orfani non appena Deleted
viene rilevata la modifica della relazione. Tuttavia, questo processo può essere ritardato fino a quando saveChanges non viene effettivamente chiamato. Ciò può essere utile per evitare di rendere orfane le entità che sono state rimosse da un principale o genitore, ma verranno riassociate a un nuovo principale/genitore prima di chiamare SaveChanges.
ChangeTracker.DeleteOrphansTiming viene usato per impostare questo intervallo. Per esempio:
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
dotNetBlog.Posts.Add(post);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Dopo aver rimosso il post dalla prima raccolta, l'oggetto non viene contrassegnato come Deleted
nell'esempio precedente. EF Core sta invece verificando che la relazione venga interrotta anche se si tratta di una relazione obbligatoria. Il valore FK viene considerato null da EF Core anche se non può essere veramente Null perché il tipo non è nullable. Questo valore è noto come "null concettuale".
Post {Id: 3} Modified
Id: 3 PK
BlogId: <null> FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: []
La chiamata a SaveChanges in questo momento comporta l'eliminazione del post orfano. Tuttavia, se come nell'esempio precedente, il post è associato a un nuovo blog prima di chiamare SaveChanges, verrà corretto in modo appropriato a quel nuovo blog e non è più considerato un orfano:
Post {Id: 3} Modified
Id: 3 PK
BlogId: 1 FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 1}
Tags: []
SaveChanges chiamato a questo punto aggiornerà il post nel database anziché eliminarlo.
È anche possibile disattivare l'eliminazione automatica degli orfani. Verrà generata un'eccezione se SaveChanges viene chiamato mentre viene rilevato un orfano. Ad esempio, questo codice:
var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;
var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);
await context.SaveChangesAsync(); // Throws
Lancerà questa eccezione:
System.InvalidOperationException: L'associazione tra entità 'Blog' e 'Post' con il valore chiave '{BlogId: 1}' è stata interrotta, ma la relazione è contrassegnata come obbligatoria o è implicitamente obbligatoria perché la chiave esterna non è annullabile. Se l'entità dipendente/figlio deve essere eliminata quando viene interrotta una relazione obbligatoria, configurare la relazione per l'uso delle eliminazioni a catena.
L'eliminazione di orfani, così come le eliminazioni a catena, può essere forzata in qualsiasi momento chiamando ChangeTracker.CascadeChanges(). Combinando questa operazione con l'impostazione del tempo di eliminazione degli orfani a Never
si garantisce che gli orfani non vengano mai eliminati, a meno che non venga esplicitamente richiesto a EF Core di farlo.
Modifica di una navigazione di riferimento
La modifica della navigazione di riferimento di una relazione uno-a-molti ha lo stesso effetto della modifica della navigazione della raccolta dall'altra parte della relazione. L'impostazione del riferimento di navigazione di dipendente/figlio a null equivale a rimuovere l'entità dalla raccolta di navigazione del principale/padre. Tutte le modifiche di correzione e al database vengono apportate come descritto nella sezione precedente, incluso rendere l'entità un orfano se la relazione è obbligatoria.
Relazioni uno-a-uno facoltative
Per le relazioni uno-a-uno, la modifica della navigazione di riferimento comporta l'interruzione di qualsiasi relazione precedente. Per le relazioni facoltative, questo significa che il valore FK nel dipendente o nel figlio precedentemente correlato è impostato su null. Per esempio:
using var context = new BlogsContext();
var dotNetBlog = await context.Blogs.Include(e => e.Assets).SingleAsync(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
La visualizzazione di debug prima di chiamare SaveChanges mostra che i nuovi asset hanno sostituito gli asset esistenti, contrassegnati come Modified
con un valore FK null BlogAssets.BlogId
:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: -2147482629}
Posts: []
BlogAssets {Id: -2147482629} Added
Id: -2147482629 PK Temporary
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 1} Modified
Id: 1 PK
Banner: <null>
BlogId: <null> FK Modified Originally 1
Blog: <null>
Ciò comporta un aggiornamento e un inserimento quando viene chiamato SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Relazioni uno-a-uno obbligatorie
Eseguendo lo stesso codice dell'esempio precedente, ma questa volta con una relazione uno-a-uno richiesta, indica che l'oggetto precedentemente associato BlogAssets
è ora contrassegnato come Deleted
, poiché diventa orfano quando il nuovo BlogAssets
prende il suo posto.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Assets: {Id: -2147482639}
Posts: []
BlogAssets {Id: -2147482639} Added
Id: -2147482639 PK Temporary
Banner: <null>
BlogId: 1 FK
Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
Id: 1 PK
Banner: <null>
BlogId: 1 FK
Blog: <null>
Viene quindi generata un'eliminazione e un inserimento quando si chiama SaveChanges:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
L'intervallo per contrassegnare gli orfani come eliminati può essere modificato nello stesso modo illustrato per le navigazioni delle raccolte e ha gli stessi effetti.
Eliminazione di un'entità
Relazioni facoltative
Quando un'entità viene contrassegnata come Deleted
, ad esempio chiamando DbContext.Remove, i riferimenti all'entità eliminata vengono rimossi dagli spostamenti di altre entità. Per le relazioni facoltative, i valori FK nelle entità dipendenti vengono impostati su Null.
Ad esempio, contrassegnare il blog di Visual Studio come Deleted
:
using var context = new BlogsContext();
var vsBlog = await context.Blogs
.Include(e => e.Posts)
.Include(e => e.Assets)
.SingleAsync(e => e.Name == "Visual Studio Blog");
context.Remove(vsBlog);
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
await context.SaveChangesAsync();
Guardare la vista di debug del tracciatore di modifiche prima di chiamare SaveChanges mostra:
Blog {Id: 2} Deleted
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
Id: 2 PK
Banner: <null>
BlogId: <null> FK Modified Originally 2
Blog: <null>
Post {Id: 3} Modified
Id: 3 PK
BlogId: <null> FK Modified Originally 2
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: []
Post {Id: 4} Modified
Id: 4 PK
BlogId: <null> FK Modified Originally 2
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: <null>
Tags: []
Si noti che:
- Il blog è contrassegnato come
Deleted
. - Gli asset correlati al blog eliminato hanno un valore FK null (
BlogId: <null> FK Modified Originally 2
) e una navigazione di riferimento Null (Blog: <null>
) - Ogni post correlato al blog eliminato ha un valore FK null (
BlogId: <null> FK Modified Originally 2
) e una navigazione di riferimento Null (Blog: <null>
)
Relazioni obbligatorie
Il comportamento di correzione per le relazioni obbligatorie è uguale a quello delle relazioni facoltative, ad eccezione del fatto che le entità dipendenti/figlio sono contrassegnate come Deleted
poiché non possono esistere senza un'entità/padre e devono essere rimosse dal database quando SaveChanges viene chiamato per evitare un'eccezione di vincolo referenziale. Questa operazione è nota come "eliminazione a catena" ed è il comportamento predefinito in EF Core per le relazioni necessarie. Ad esempio, l'esecuzione dello stesso codice dell'esempio precedente ma con una relazione obbligatoria comporta la visualizzazione di debug seguente prima di chiamare SaveChanges:
Blog {Id: 2} Deleted
Id: 2 PK
Name: 'Visual Studio Blog'
Assets: {Id: 2}
Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
Id: 2 PK
Banner: <null>
BlogId: 2 FK
Blog: {Id: 2}
Post {Id: 3} Deleted
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: {Id: 2}
Tags: []
Post {Id: 4} Deleted
Id: 4 PK
BlogId: 2 FK
Content: 'Examine when database queries were executed and measure how ...'
Title: 'Database Profiling with Visual Studio'
Blog: {Id: 2}
Tags: []
Come previsto, i dipendenti/figli sono ora contrassegnati come Deleted
. Si noti tuttavia che le navigazioni sulle entità eliminate non sono state modificate. Questo può sembrare strano, ma evita di frammentare completamente un grafo eliminato di entità cancellando tutte le navigazioni. Ovvero, il blog, l'asset e i post formano ancora un grafico di entità anche dopo l'eliminazione. In questo modo, è molto più semplice ripristinare un grafo di entità rispetto al caso di EF6, in cui il grafo è stato frammentato.
Tempistica di eliminazione a cascata e riassegnazione dei genitori
Per impostazione predefinita, l'eliminazione a catena viene eseguita non appena l'elemento padre/entità viene contrassegnato come Deleted
. Questo vale per l'eliminazione di orfani, come descritto in precedenza. Come per l'eliminazione di orfani, questo processo può essere ritardato fino a quando saveChanges non viene chiamato o disabilitato completamente impostando ChangeTracker.CascadeDeleteTiming in modo appropriato. Questo è utile allo stesso modo in cui viene usato per eliminare gli orfani, compresa la riassegnazione di figli/dipendenti dopo l'eliminazione di un'entità o elemento principale/padre.
Le eliminazioni a cascata, così come l'eliminazione di orfani, possono essere forzate in qualsiasi momento chiamando ChangeTracker.CascadeChanges(). La combinazione di questa operazione con l'impostazione dell'intervallo di eliminazione a catena per Never
garantisce che le eliminazioni a catena non vengano mai eseguite a meno che EF Core non venga esplicitamente richiesto di farlo.
Suggerimento
L'eliminazione a catena e l'eliminazione degli orfani sono strettamente correlate. Entrambi comportano l'eliminazione di entità dipendenti/figlio quando la relazione con l'entità principale/padre richiesta viene interrotta. Per l'eliminazione a catena, questa interruzione avviene perché l'entità principale/genitore viene eliminata. Per gli orfani, l'entità principale/padre esiste ancora, ma non è più correlata alle entità dipendenti/figlio.
Relazioni molti-a-molti
Le relazioni molti-a-molti in EF Core vengono implementate usando un'entità di collegamento. Ogni lato della relazione molti-a-molti è correlato a questa entità di collegamento con una relazione uno-a-molti. Questa entità join può essere definita e mappata in modo esplicito oppure può essere creata in modo implicito e nascosto. In entrambi i casi il comportamento sottostante è lo stesso. Innanzitutto esamineremo questo comportamento sottostante per comprendere il funzionamento del rilevamento delle relazioni molti-a-molti.
Come funzionano le relazioni molti-a-molti
Si consideri questo modello di EF Core che crea una relazione molti-a-molti tra post e tag usando un tipo di entità join definito in modo esplicito:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public Post Post { get; set; } // Reference navigation
public Tag Tag { get; set; } // Reference navigation
}
Si noti che il PostTag
tipo di entità join contiene due proprietà di chiave esterna. In questo modello, affinché un post sia correlato a un tag, deve essere presente un'entità join PostTag in cui il valore della PostTag.PostId
chiave esterna corrisponde al valore della Post.Id
chiave primaria e dove il valore della PostTag.TagId
chiave esterna corrisponde al valore della Tag.Id
chiave primaria. Per esempio:
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Esaminando la visualizzazione debug di Rilevamento modifiche dopo l'esecuzione di questo codice, il post e il tag sono collegati dalla nuova entità join PostTag
.
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
PostId: 3 PK FK
TagId: 1 PK FK
Post: {Id: 3}
Tag: {Id: 1}
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
PostTags: [{PostId: 3, TagId: 1}]
Si noti che le navigazioni della raccolta su Post
e Tag
sono state corrette, come anche le navigazioni di riferimento su PostTag
. Queste relazioni possono essere modificate dagli spostamenti anziché dai valori FK, come in tutti gli esempi precedenti. Ad esempio, il codice precedente può essere modificato per aggiungere la relazione impostando gli spostamenti di riferimento nell'entità join:
context.Add(new PostTag { Post = post, Tag = tag });
Ciò comporta esattamente la stessa modifica apportata agli FK e agli spostamenti dell'esempio precedente.
Salta le navigazioni
La modifica manuale della tabella di collegamento può essere scomoda. Le relazioni molti-a-molti possono essere modificate direttamente usando navigazioni di raccolte speciali che "ignorano" l'entità join. Ad esempio, al modello precedente possono essere aggiunte due navigazioni skip: una da Post a Tag e l'altra da Tag a Post.
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public Post Post { get; set; } // Reference navigation
public Tag Tag { get; set; } // Reference navigation
}
Questa relazione molti-a-molti richiede la configurazione seguente per garantire che le navigazioni saltate e le navigazioni standard siano tutte utilizzate per la stessa relazione molti-a-molti.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}
Per ulteriori informazioni sul mapping delle relazioni molti-a-molti, vedere Relazioni.
Le navigazioni di salto sembrano e si comportano come normali navigazioni delle raccolte. Tuttavia, il modo in cui funzionano con i valori di chiave esterna è diverso. Associare un post a un tag, ma questa volta usando una navigazione skip:
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Si noti che questo codice non usa l'entità join. Invece aggiunge un'entità a una raccolta di navigazione, esattamente come si farebbe se fosse una relazione di tipo uno-a-molti. La visualizzazione di debug risultante è essenzialmente la stessa di prima:
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
PostTags: [{PostId: 3, TagId: 1}]
Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
PostId: 3 PK FK
TagId: 1 PK FK
Post: {Id: 3}
Tag: {Id: 1}
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
PostTags: [{PostId: 3, TagId: 1}]
Posts: [{Id: 3}]
Si noti che un'istanza dell'entità di join PostTag
è stata creata automaticamente perché i valori FK sono stati impostati sui valori PK del tag e del post che ora sono associati. Tutte le navigazioni normali di riferimento e raccolta sono state corrette per corrispondere a questi valori FK. Inoltre, poiché questo modello contiene navigazioni skip, anche queste sono state corrette. In particolare, anche se è stato aggiunto il tag alla Post.Tags
skip navigazione, anche la Tag.Posts
navigazione skip inversa dall'altro lato di questa relazione è stata corretta per contenere il post associato.
Vale la pena notare che le relazioni molti-a-molti sottostanti possono comunque essere modificate direttamente anche quando le navigazioni ignorate sono state applicate sopra. Ad esempio, il tag e il Post possono essere associati così come abbiamo fatto prima di introdurre le navigazioni di salto.
context.Add(new PostTag { Post = post, Tag = tag });
Oppure usando i valori FK:
context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });
Ciò continuerà a comportare la corretta sistemazione dei salti di navigazione, risultando nello stesso output della visualizzazione di debug dell'esempio precedente.
Ignora solo le navigazioni
Nella sezione precedente abbiamo aggiunto la navigazione di avvio oltre a una completa definizione delle due relazioni uno-a-molti che sono alla base di. Ciò è utile per illustrare cosa accade ai valori FK, ma spesso non è necessario. La relazione molti-a-molti può invece essere definita usando solo navigazioni skip. Questo è il modo in cui la relazione molti-a-molti è definita nel modello nella parte superiore di questo documento. Usando questo modello, è possibile associare nuovamente un post e un tag aggiungendo un post alla Tag.Posts
navigazione skip (o, in alternativa, aggiungendo un tag alla Post.Tags
navigazione skip):
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Esaminando la visualizzazione di debug dopo aver apportato questa modifica, EF Core ha creato un'istanza di Dictionary<string, object>
per rappresentare l'entità join. Questa entità join contiene sia le proprietà di chiave esterna PostsId
che TagsId
, che sono state impostate in modo tale da corrispondere ai valori PK del post e del tag associati.
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
PostsId: 3 PK FK
TagsId: 1 PK FK
Per altre informazioni sulle entità di join implicite e sull'uso dei tipi di entità, vedere Dictionary<string, object>
.
Importante
Il tipo CLR usato per i tipi di entità join per convenzione può cambiare nelle versioni future per migliorare le prestazioni. Non dipendere dal tipo di join Dictionary<string, object>
a meno che non sia stato configurato esplicitamente.
Unire entità con payload
Finora tutti gli esempi hanno usato un tipo di entità join (esplicito o implicito) che contiene solo le due proprietà di chiave esterna necessarie per la relazione molti-a-molti. Nessuno di questi valori FK deve essere impostato in modo esplicito dall'applicazione durante la modifica delle relazioni perché i relativi valori provengono dalle proprietà della chiave primaria delle entità correlate. Ciò consente a EF Core di creare istanze dell'entità di join senza dati mancanti.
Payload con valori generati
EF Core supporta l'aggiunta di proprietà aggiuntive al tipo di entità join. Questa operazione è nota come assegnare un "payload" all'entità di join. Ad esempio, aggiungiamo la proprietà TaggedOn
all'entità PostTag
join.
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public DateTime TaggedOn { get; set; } // Payload
}
Questa proprietà del carico non verrà impostata quando EF Core crea un'istanza dell'entità di unione. Il modo più comune per gestire questa operazione consiste nell'usare le proprietà del payload con valori generati automaticamente. Ad esempio, la TaggedOn
proprietà può essere configurata per l'uso di un timestamp generato dall'archivio quando viene inserita ogni nuova entità:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j.HasOne<Tag>().WithMany(),
j => j.HasOne<Post>().WithMany(),
j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}
Un post può ora essere contrassegnato nello stesso modo di prima:
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
post.Tags.Add(tag);
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Esaminando la visualizzazione debug del rilevatore di modifiche dopo aver chiamato il metodo SaveChanges, si mostra che la proprietà payload è stata impostata correttamente.
Post {Id: 3} Unchanged
Id: 3 PK
BlogId: 2 FK
Content: 'If you are focused on squeezing out the last bits of perform...'
Title: 'Disassembly improvements for optimized managed debugging'
Blog: <null>
Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
PostId: 3 PK FK
TagId: 1 PK FK
TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
Id: 1 PK
Text: '.NET'
Posts: [{Id: 3}]
Impostazione esplicita dei valori del payload
Seguendo l'esempio precedente, si aggiungerà una proprietà payload che non usa un valore generato automaticamente:
public class PostTag
{
public int PostId { get; set; } // First part of composite PK; FK to Post
public int TagId { get; set; } // Second part of composite PK; FK to Tag
public DateTime TaggedOn { get; set; } // Auto-generated payload property
public string TaggedBy { get; set; } // Not-generated payload property
}
Un post può ora essere contrassegnato nello stesso modo di prima e l'entità join verrà comunque creata automaticamente. È quindi possibile accedere a questa entità usando uno dei meccanismi descritti in Accesso alle entità rilevate. Ad esempio, il codice seguente usa DbSet<TEntity>.Find per accedere all'istanza dell'entità join:
using var context = new BlogsContext();
var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);
post.Tags.Add(tag);
context.ChangeTracker.DetectChanges();
var joinEntity = await context.Set<PostTag>().FindAsync(post.Id, tag.Id);
joinEntity.TaggedBy = "ajcvickers";
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Dopo aver individuato l'entità join, è possibile modificarla nel modo normale, in questo esempio, per impostare la TaggedBy
proprietà payload prima di chiamare SaveChanges.
Annotazioni
Si noti che qui è necessaria una chiamata a ChangeTracker.DetectChanges() per consentire a EF Core di rilevare la modifica della proprietà di navigazione e creare l'istanza dell'entità join prima Find
di essere usata. Per altre informazioni, vedere Rilevamento modifiche e notifiche .
In alternativa, l'entità join può essere creata in modo esplicito per associare un post a un tag. Per esempio:
using var context = new BlogsContext();
var post = context.Posts.SingleAsync(e => e.Id == 3);
var tag = context.Tags.SingleAsync(e => e.Id == 1);
context.Add(
new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });
await context.SaveChangesAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Infine, un altro modo per impostare i dati di payload consiste nell'eseguire l'override SaveChanges o usare l'evento DbContext.SavingChanges per elaborare le entità prima di aggiornare il database. Per esempio:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
{
if (entityEntry.State == EntityState.Added)
{
entityEntry.Entity.TaggedBy = "ajcvickers";
}
}
return await base.SaveChangesAsync(cancellationToken);
}