ExecuteUpdate e ExecuteDelete
Nota
Questa funzionalità è stata introdotta in EF Core 7.0.
ExecuteUpdate e ExecuteDelete sono un modo per salvare i dati nel database senza usare il tradizionale rilevamento delle modifiche e SaveChanges() il metodo di Entity Framework. Per un confronto introduttivo di queste due tecniche, vedere la pagina Panoramica sul salvataggio dei dati.
ExecuteDelete
Si supponga di dover eliminare tutti i blog con una classificazione inferiore a una determinata soglia. L'approccio tradizionale SaveChanges() richiede di eseguire le operazioni seguenti:
await foreach (var blog in context.Blogs.Where(b => b.Rating < 3).AsAsyncEnumerable())
{
context.Blogs.Remove(blog);
}
await context.SaveChangesAsync();
Si tratta di un modo abbastanza inefficiente per eseguire questa attività: si eseguono query sul database per tutti i blog corrispondenti al filtro e quindi si eseguono query, materializzano e tengono traccia di tutte queste istanze; il numero di entità corrispondenti potrebbe essere enorme. Si indica quindi al tracker delle modifiche di ENTITY che ogni blog deve essere rimosso e di applicare tali modifiche chiamando SaveChanges(), che genera un'istruzione DELETE
per ognuno di essi e ognuno di essi.
Ecco la stessa attività eseguita tramite l'API ExecuteDelete :
await context.Blogs.Where(b => b.Rating < 3).ExecuteDeleteAsync();
In questo modo si usano gli operatori LINQ familiari per determinare quali blog devono essere interessati, come se si stesse eseguendo una query su di essi, e quindi indirà a EF di eseguire un'istanza di SQL DELETE
nel database:
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Oltre a essere più semplice e più breve, questo viene eseguito in modo molto efficiente nel database, senza caricare dati dal database o coinvolgere lo strumento di rilevamento delle modifiche di ENTITY. Si noti che è possibile usare operatori LINQ arbitrari per selezionare i blog da eliminare: questi vengono convertiti in SQL per l'esecuzione nel database, come se si stesse eseguendo una query su tali blog.
ExecuteUpdate
Invece di eliminare questi blog, cosa accade se si vuole modificare una proprietà per indicare che devono essere nascosti? ExecuteUpdate offre un modo simile per esprimere un'istruzione SQL UPDATE
:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.IsVisible, false));
Come con ExecuteDelete
, usiamo prima LINQ per determinare quali blog devono essere interessati, ma con ExecuteUpdate
dobbiamo anche esprimere la modifica da applicare ai blog corrispondenti. Questa operazione viene eseguita chiamando SetProperty
all'interno della ExecuteUpdate
chiamata e fornendo due argomenti: la proprietà da modificare (IsVisible
) e il nuovo valore che deve avere (false
). In questo modo viene eseguito il codice SQL seguente:
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Aggiornamento di più proprietà
ExecuteUpdate
consente di aggiornare più proprietà in una singola chiamata. Ad esempio, per impostare IsVisible
su false e per impostare Rating
su zero, è sufficiente concatenare chiamate aggiuntive SetProperty
insieme:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));
In questo modo viene eseguito il codice SQL seguente:
UPDATE [b]
SET [b].[Rating] = 0,
[b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Riferimento al valore della proprietà esistente
Negli esempi precedenti la proprietà è stata aggiornata a un nuovo valore costante. ExecuteUpdate
consente inoltre di fare riferimento al valore della proprietà esistente durante il calcolo del nuovo valore; Ad esempio, per aumentare la classificazione di tutti i blog corrispondenti di uno, usare quanto segue:
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
Si noti che il secondo argomento di SetProperty
è ora una funzione lambda e non una costante come in precedenza. Il b
parametro rappresenta il blog da aggiornare; all'interno di tale espressione lambda, b.Rating
contiene quindi la classificazione prima di qualsiasi modifica apportata. In questo modo viene eseguito il codice SQL seguente:
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Spostamenti ed entità correlate
ExecuteUpdate
attualmente non supporta il riferimento agli spostamenti all'interno dell'espressione SetProperty
lambda. Si supponga, ad esempio, di voler aggiornare tutte le classificazioni dei blog in modo che la nuova classificazione di ogni blog sia la media di tutte le classificazioni dei post. È possibile provare a usare ExecuteUpdate
come segue:
await context.Blogs.ExecuteUpdateAsync(
setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));
Ef, tuttavia, consente di eseguire questa operazione usando Select
prima di tutto per calcolare la valutazione media e proiettarla in un tipo anonimo e quindi usando ExecuteUpdate
su di esso:
await context.Blogs
.Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Blog.Rating, b => b.NewRating));
In questo modo viene eseguito il codice SQL seguente:
UPDATE [b]
SET [b].[Rating] = CAST((
SELECT AVG(CAST([p].[Rating] AS float))
FROM [Post] AS [p]
WHERE [b].[Id] = [p].[BlogId]) AS int)
FROM [Blogs] AS [b]
Rilevamento modifiche
Gli utenti che hanno familiarità con SaveChanges
vengono usati per eseguire più modifiche e quindi chiamare SaveChanges
per applicare tutte queste modifiche al database. Ciò è reso possibile dallo strumento di rilevamento delle modifiche di Entity Framework, che accumula o tiene traccia di queste modifiche.
ExecuteUpdate
e ExecuteDelete
funzionano in modo molto diverso: diventano effettive immediatamente, nel momento in cui vengono richiamate. Ciò significa che mentre una singola ExecuteUpdate
operazione o ExecuteDelete
può influire su molte righe, non è possibile accumulare più operazioni di questo tipo e applicarle contemporaneamente, ad esempio quando si chiama SaveChanges
. Infatti, le funzioni non sono completamente a conoscenza del tracker delle modifiche di EF e non hanno alcuna interazione con esso. Questo ha diverse conseguenze importanti.
Si consideri il seguente codice :
// 1. Query the blog with the name `SomeBlog`. Since EF queries are tracking by default, the Blog is now tracked by EF's change tracker.
var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
// 2. Increase the rating of all blogs in the database by one. This executes immediately.
await context.Blogs.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
// 3. Increase the rating of `SomeBlog` by two. This modifies the .NET `Rating` property and is not yet persisted to the database.
blog.Rating += 2;
// 4. Persist tracked changes to the database.
await context.SaveChangesAsync();
In modo cruciale, quando ExecuteUpdate
viene richiamato e tutti i blog vengono aggiornati nel database, lo strumento di rilevamento delle modifiche di Entity Framework non viene aggiornato e l'istanza di .NET rilevata ha ancora il valore di classificazione originale, dal punto in cui è stata eseguita una query. Si supponga che la valutazione del blog sia stata originariamente 5; dopo l'esecuzione della terza riga, la classificazione nel database è ora 6 (a causa di ExecuteUpdate
), mentre la classificazione nell'istanza di .NET rilevata è 7. Quando SaveChanges
viene chiamato, Entity Framework rileva che il nuovo valore 7 è diverso dal valore originale 5 e mantiene tale modifica. La modifica eseguita da ExecuteUpdate
viene sovrascritta e non presa in considerazione.
Di conseguenza, è in genere consigliabile evitare di combinare sia le modifiche rilevate SaveChanges
che le modifiche non registrate tramite/ExecuteUpdate
ExecuteDelete
.
Transazioni
Continuando con quanto sopra, è importante comprendere che ExecuteUpdate
e ExecuteDelete
non avviare in modo implicito una transazione che vengono richiamate. Si consideri il seguente codice :
await context.Blogs.ExecuteUpdateAsync(/* some update */);
await context.Blogs.ExecuteUpdateAsync(/* another update */);
var blog = await context.Blogs.SingleAsync(b => b.Name == "SomeBlog");
blog.Rating += 2;
await context.SaveChangesAsync();
Ogni ExecuteUpdate
chiamata fa sì che un singolo SQL UPDATE
venga inviato al database. Poiché non viene creata alcuna transazione, se un tipo di errore impedisce il completamento del secondo ExecuteUpdate
, gli effetti del primo vengono comunque mantenuti nel database. Infatti, le quattro operazioni precedenti, due chiamate di ExecuteUpdate
, una query e SaveChanges
, ognuna viene eseguita all'interno della propria transazione. Per eseguire il wrapping di più operazioni in una singola transazione, avviare in modo esplicito una transazione con DatabaseFacade:
using (var transaction = context.Database.BeginTransaction())
{
context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);
...
}
Per altre informazioni sulla gestione delle transazioni, vedere Uso delle transazioni.
Controllo della concorrenza e righe interessate
SaveChanges
fornisce il controllo di concorrenza automatico, usando un token di concorrenza per assicurarsi che una riga non sia stata modificata tra il momento in cui è stata caricata e il momento in cui si salvano le modifiche. Poiché ExecuteUpdate
e ExecuteDelete
non interagiscono con lo strumento di rilevamento modifiche, non possono applicare automaticamente il controllo della concorrenza.
Tuttavia, entrambi questi metodi restituiscono il numero di righe interessate dall'operazione; questo può risultare particolarmente utile per implementare manualmente il controllo della concorrenza:
// (load the ID and concurrency token for a Blog in the database)
var numUpdated = await context.Blogs
.Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
.ExecuteUpdateAsync(/* ... */);
if (numUpdated == 0)
{
throw new Exception("Update failed!");
}
In questo codice si usa un operatore LINQ Where
per applicare un aggiornamento a un blog specifico e solo se il token di concorrenza ha un valore specifico, ad esempio quello visualizzato durante l'esecuzione di query sul blog dal database. Viene quindi verificato il numero di righe effettivamente aggiornate da ExecuteUpdate
. Se il risultato è zero, non sono state aggiornate righe e il token di concorrenza è stato probabilmente modificato in seguito a un aggiornamento simultaneo.
Limiti
- Attualmente è supportato solo l'aggiornamento e l'eliminazione; l'inserimento deve essere eseguito tramite DbSet<TEntity>.Add e SaveChanges().
- Mentre le istruzioni SQL UPDATE e DELETE consentono di recuperare i valori di colonna originali per le righe interessate, questo non è attualmente supportato da
ExecuteUpdate
eExecuteDelete
. - Non è possibile eseguire l'invio in batch di più chiamate di questi metodi. Ogni chiamata esegue il proprio round trip nel database.
- I database consentono in genere di modificare solo una singola tabella con UPDATE o DELETE.
- Questi metodi attualmente funzionano solo con i provider di database relazionali.
Risorse aggiuntive
- Sessione di standup community di accesso ai dati .NET in cui vengono illustrati
ExecuteUpdate
eExecuteDelete
.