Condividi tramite


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:

foreach (var blog in context.Blogs.Where(b => b.Rating < 3))
{
    context.Blogs.Remove(blog);
}

context.SaveChanges();

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 :

context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();

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 :

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(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:

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(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:

context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdate(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

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:

context.Blogs.ExecuteUpdate(
    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:

context.Blogs
    .Select(b => new { Blog = b, NewRating = b.Posts.Average(p => p.Rating) })
    .ExecuteUpdate(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.

Osservare il codice seguente:

// 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 = context.Blogs.Single(b => b.Name == "SomeBlog");

// 2. Increase the rating of all blogs in the database by one. This executes immediately.
context.Blogs.ExecuteUpdate(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.
context.SaveChanges();

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/ExecuteUpdateExecuteDelete .

Transazioni

Continuando con quanto sopra, è importante comprendere che ExecuteUpdate e ExecuteDelete non avviare in modo implicito una transazione che vengono richiamate. Osservare il codice seguente:

context.Blogs.ExecuteUpdate(/* some update */);
context.Blogs.ExecuteUpdate(/* another update */);

var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();

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

SaveChangesfornisce 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 = context.Blogs
    .Where(b => b.Id == id && b.ConcurrencyToken == concurrencyToken)
    .ExecuteUpdate(/* ... */);
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 e ExecuteDelete.
  • 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 e ExecuteDelete.