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.
Un'istanza DbContext tiene traccia automaticamente delle entità restituite dal database. Le modifiche apportate a queste entità verranno quindi rilevate quando viene chiamato SaveChanges e il database verrà aggiornato in base alle esigenze. Per informazioni dettagliate, vedere Salvataggio di base e dati correlati .
Tuttavia, a volte le entità vengono sottoposte a query usando un'istanza di contesto e quindi salvate usando un'istanza diversa. Questo accade spesso in scenari "disconnessi", ad esempio un'applicazione Web in cui vengono eseguite query sulle entità, inviate al client, modificate, inviate al server in una richiesta e quindi salvate. In questo caso, la seconda istanza di contesto deve sapere se le entità sono nuove (devono essere inserite) o esistenti (devono essere aggiornate).
Suggerimento
È possibile visualizzare l'esempio di questo articolo in GitHub.
Suggerimento
EF Core può tenere traccia di un'istanza di qualsiasi entità con un determinato valore di chiave primaria. Il modo migliore per evitare che si tratti di un problema consiste nell'usare un contesto di breve durata per ogni unità-di-lavoro, in modo che il contesto inizi vuoto, abbia entità associate, salvi quelle entità e poi venga dismesso e scartato.
Identificazione di nuove entità
Il client identifica le nuove entità
Il caso più semplice da gestire è quando il client informa il server se l'entità è nuova o esistente. Ad esempio, spesso la richiesta di inserire una nuova entità è diversa dalla richiesta per aggiornare un'entità esistente.
Nella parte restante di questa sezione vengono illustrati i casi in cui è necessario determinare in altro modo se inserire o aggiornare.
Con chiavi generate automaticamente
Il valore di una chiave generata automaticamente può spesso essere usato per determinare se un'entità deve essere inserita o aggiornata. Se la chiave non è stata impostata, ovvero ha ancora il valore predefinito CLR null, zero e così via, l'entità deve essere nuova e deve essere inserita. D'altra parte, se il valore della chiave è stato impostato, deve essere già stato salvato in precedenza e ora richiede l'aggiornamento. In altre parole, se la chiave ha un valore, l'entità è stata oggetto di una query, inviata al client ed è ora ritornata per essere aggiornata.
È facile verificare la presenza di una chiave unset quando il tipo di entità è noto:
public static bool IsItNew(Blog blog)
=> blog.BlogId == 0;
Ef, tuttavia, offre anche un modo predefinito per eseguire questa operazione per qualsiasi tipo di entità e tipo di chiave:
public static bool IsItNew(DbContext context, object entity)
=> !context.Entry(entity).IsKeySet;
Suggerimento
Le chiavi vengono impostate non appena le entità vengono rilevate dal contesto, anche se l'entità si trova nello stato Aggiunto. Ciò consente di attraversare un grafico di entità e decidere cosa fare con ogni entità, ad esempio quando si usa l'API TrackGraph. Il valore della chiave deve essere usato solo nel modo illustrato qui prima che venga effettuata una chiamata per tenere traccia dell'entità.
Con altre chiavi
È necessario un altro meccanismo per identificare le nuove entità quando i valori chiave non vengono generati automaticamente. Esistono due approcci generali a questo scopo:
- Query per l'entità
- Passare un flag dal client
Per eseguire una query per l'entità, usare il metodo Find:
public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
=> (await context.Blogs.FindAsync(blog.BlogId)) == null;
Risulta al di fuori dell'ambito di questo documento mostrare il codice completo per il passaggio di un flag da un client. In un'app Web, in genere significa effettuare richieste diverse per azioni diverse o passare uno stato nella richiesta e quindi estrarlo nel controller.
Salvataggio di singole entità
Se è noto se è necessario o meno un inserimento o un aggiornamento, è possibile usare l'opzione Aggiungi o Aggiorna in modo appropriato:
public static async Task Insert(DbContext context, object entity)
{
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task Update(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
Tuttavia, se l'entità usa valori di chiave generati automaticamente, il metodo Update può essere usato per entrambi i casi:
public static async Task InsertOrUpdate(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
Il metodo Update contrassegna normalmente l'entità per l'aggiornamento, non l'inserimento. Tuttavia, se l'entità ha una chiave generata automaticamente e non è stato impostato alcun valore di chiave, l'entità viene invece contrassegnata automaticamente per l'inserimento.
Se l'entità non usa chiavi generate automaticamente, l'applicazione deve decidere se l'entità deve essere inserita o aggiornata: ad esempio:
public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
}
await context.SaveChangesAsync();
}
Di seguito sono riportati i passaggi seguenti:
- Se il metodo Find restituisce null, allora il database non contiene già il blog con questo ID, quindi chiamiamo Add per contrassegnarlo per l'inserimento.
- Se Find restituisce un'entità, esiste nel database e il contesto ora monitora l'entità esistente
- Si usa quindi SetValues per impostare i valori per tutte le proprietà di questa entità su quelle provenienti dal client.
- La chiamata SetValues contrassegnerà l'entità da aggiornare in base alle esigenze.
Suggerimento
SetValues contrassegna solo come modificate le proprietà con valori diversi rispetto a quelli nell'entità rilevata. Ciò significa che quando viene inviato l'aggiornamento, verranno aggiornate solo le colonne effettivamente modificate. Se non è stato modificato nulla, non verrà inviato alcun aggiornamento.
Uso dei grafici
Risoluzione di identità
Come indicato in precedenza, EF Core può tenere traccia di una sola istanza di qualsiasi entità con un determinato valore di chiave primaria. Quando si usano grafici, è consigliabile creare un grafico in modo che venga mantenuto questo invariante e che il contesto venga usato per una sola unità di lavoro. Se il grafico contiene duplicati, sarà necessario elaborare il grafico prima di inviarlo a Entity Framework per consolidare più istanze in una. Questo potrebbe non essere semplice quando le istanze hanno valori e relazioni in conflitto, quindi il consolidamento dei duplicati deve essere eseguito il prima possibile nella pipeline dell'applicazione per evitare la risoluzione dei conflitti.
Tutte le entità nuove/tutte esistenti
Un esempio di utilizzo dei grafici consiste nell'inserimento o nell'aggiornamento di un blog insieme alla raccolta di post associati. Se tutte le entità nel grafico devono essere inserite o tutte devono essere aggiornate, il processo è uguale a quello descritto in precedenza per le singole entità. Ad esempio, un grafico di blog e post creati in questo modo:
var blog = new Blog
{
Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};
può essere inserito come segue:
public static async Task InsertGraph(DbContext context, object rootEntity)
{
context.Add(rootEntity);
await context.SaveChangesAsync();
}
La chiamata a Aggiungi contrassegnerà il blog e tutti i post da inserire.
Analogamente, se tutte le entità in un grafico devono essere aggiornate, è possibile usare Update:
public static async Task UpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
Il blog e tutti i relativi post verranno contrassegnati per essere aggiornati.
Combinazione di entità nuove ed esistenti
Con le chiavi generate automaticamente, è possibile usare di nuovo Update sia per gli inserimenti che per gli aggiornamenti, anche se il grafico contiene una combinazione di entità che richiedono l'inserimento e quelle che richiedono l'aggiornamento:
public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
L'aggiornamento contrassegnerà qualsiasi entità nel grafico, nel blog o nel post, per l'inserimento se non ha un valore chiave impostato, mentre tutte le altre entità sono contrassegnate per l'aggiornamento.
Come in precedenza, quando non si usano chiavi generate automaticamente, si possono usare una query e alcune elaborazioni.
public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
}
await context.SaveChangesAsync();
}
Gestione delle cancellazioni
L'eliminazione può essere difficile da gestire perché spesso l'assenza di un'entità significa che deve essere eliminata. Un modo per gestire questa operazione consiste nell'usare "eliminazioni software" in modo che l'entità sia contrassegnata come eliminata anziché essere effettivamente eliminata. Le eliminazioni diventano quindi uguali agli aggiornamenti. Le eliminazioni software possono essere implementate usando filtri di query.
Per le eliminazioni definitive, un modello comune consiste nell'usare un'estensione del modello di query per eseguire ciò che è essenzialmente una differenza nel grafico. Per esempio:
public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
await context.SaveChangesAsync();
}
TrackGraph
Internamente, Add, Attach e Update usano l'attraversamento grafico con una determinazione effettuata per ogni entità per stabilire se deve essere contrassegnata come Aggiunta (per l'inserimento), Modifica (per aggiornare), Non modifica (non eseguire alcuna operazione) o Eliminata (da eliminare). Questo meccanismo viene esposto tramite l'API TrackGraph. Si supponga, ad esempio, che quando il client restituisce un grafico di entità imposta un flag su ogni entità che indica come deve essere gestito. TrackGraph può quindi essere usato per elaborare questo flag:
public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
context.ChangeTracker.TrackGraph(
rootEntity,
n =>
{
var entity = (EntityBase)n.Entry.Entity;
n.Entry.State = entity.IsNew
? EntityState.Added
: entity.IsChanged
? EntityState.Modified
: entity.IsDeleted
? EntityState.Deleted
: EntityState.Unchanged;
});
await context.SaveChangesAsync();
}
Le bandiere vengono visualizzate solo come parte dell'entità per la semplicità dell'esempio. In genere, i flag fanno parte di un DTO o di un altro stato incluso nella richiesta.