ExecuteUpdate och ExecuteDelete

ExecuteUpdate och ExecuteDelete är ett sätt att spara data i databasen utan att använda EF:s traditionella ändringsspårning och SaveChanges() metod. En introduktionsjämförelse av dessa två tekniker finns på sidan Översikt om hur du sparar data.

ExecuteDelete

Anta att du måste ta bort alla bloggar med ett omdöme under ett visst tröskelvärde. Den traditionella SaveChanges() metoden kräver att du gör följande:

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

await context.SaveChangesAsync();

Det här är ett ganska ineffektivt sätt att utföra den här uppgiften: vi frågar databasen efter alla bloggar som matchar vårt filter, och sedan frågar vi, materialiserar och spårar alla dessa instanser; antalet matchande entiteter kan vara stort. Sedan berättar vi för EF:s ändringsspårare att varje blogg måste tas bort och tillämpar ändringarna genom att anropa SaveChanges(), vilket genererar en DELETE instruktion för var och en av dem.

Här är samma uppgift som utförs via API:et ExecuteDelete :

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

Detta använder de välbekanta LINQ-operatorerna för att avgöra vilka bloggar som ska påverkas – precis som om vi körde frågor mot dem – och uppmanar sedan EF att köra en SQL DELETE mot databasen:

DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Förutom att vara enklare och kortare körs detta mycket effektivt i databasen, utan att läsa in några data från databasen eller involvera EF:s ändringsspårare. Observera att du kan använda godtyckliga LINQ-operatorer för att välja vilka bloggar du vill ta bort – dessa översätts till SQL för körning i databasen, precis som om du frågar efter dessa bloggar.

UtförUppdatering

I stället för att ta bort dessa bloggar, vad händer om vi vill ändra en egenskap för att indikera att de bör vara dolda i stället? ExecuteUpdate ger ett liknande sätt att uttrycka en SQL-instruktion UPDATE :

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(setters => setters.SetProperty(b => b.IsVisible, false));

Precis som med ExecuteDeleteanvänder vi först LINQ för att avgöra vilka bloggar som ska påverkas, men med ExecuteUpdate måste vi också uttrycka ändringen som ska tillämpas på matchande bloggar. Detta görs genom att anropa SetProperty i anropet ExecuteUpdate och ge det två argument: egenskapen som ska ändras (IsVisible) och det nya värdet som den ska ha (false). Detta gör att följande SQL körs:

UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Uppdatera flera egenskaper

ExecuteUpdate tillåter uppdatering av flera egenskaper i ett enda anrop. Om du till exempel både anger IsVisible till false och för att ställa in Rating på noll, kedjar du helt enkelt ihop ytterligare SetProperty anrop:

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(b => b.IsVisible, false)
        .SetProperty(b => b.Rating, 0));

Detta kör följande SQL:

UPDATE [b]
SET [b].[Rating] = 0,
    [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

Hänvisa till det befintliga egenskapsvärdet

Exemplen ovan uppdaterade egenskapen till ett nytt konstant värde. ExecuteUpdate gör det också möjligt att referera till det befintliga egenskapsvärdet vid beräkning av det nya värdet. Om du till exempel vill öka klassificeringen för alla matchande bloggar med en använder du följande:

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));

Observera att det andra argumentet till SetProperty nu är en lambda-funktion och inte en konstant som tidigare. Parametern b representerar den blogg som uppdateras. I den lambda-filen b.Rating innehåller den därför omdömet innan någon ändring har gjorts. Detta kör följande SQL:

UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3

ExecuteUpdate stöder för närvarande inte referensnavigeringar i SetProperty lambda. Anta till exempel att vi vill uppdatera alla bloggklassificeringar så att varje bloggs nya omdöme är genomsnittet för alla inläggs betyg. Vi kan försöka använda ExecuteUpdate på följande sätt:

await context.Blogs.ExecuteUpdateAsync(
    setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));

EF tillåter dock att du utför den här åtgärden genom att först använda Select för att beräkna den genomsnittliga klassificeringen och projicera den till en anonym typ och sedan använda ExecuteUpdate över det:

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));

Detta kör följande SQL:

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]

Spårning av ändringar

Användare som är bekanta med SaveChanges är vana vid att utföra flera ändringar och anropar SaveChanges sedan för att tillämpa alla dessa ändringar i databasen. Detta möjliggörs av EF:s ändringsspårare, som ackumulerar eller spårar dessa ändringar.

ExecuteUpdate och ExecuteDelete fungerar helt annorlunda: de börjar gälla omedelbart, vid den punkt där de anropas. Det innebär att även om en enskild ExecuteUpdate åtgärd eller ExecuteDelete åtgärd kan påverka många rader, går det inte att ackumulera flera sådana åtgärder och tillämpa dem samtidigt, t.ex. när du anropar SaveChanges. I själva verket är funktionerna helt omedvetna om EF:s ändringsspårare och har ingen interaktion med den alls. Detta har flera viktiga konsekvenser.

Överväg följande kod:

// 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();

Avgörande är att när ExecuteUpdate anropas och alla bloggar uppdateras i databasen, uppdateras EF:s ändringsspårare inte, och den spårade .NET-instansen har fortfarande sitt ursprungliga klassificeringsvärde från den tidpunkt då den efterfrågades. Låt oss anta att bloggens omdöme ursprungligen var 5; När den tredje raden har körts är klassificeringen i databasen nu 6 (på grund av ExecuteUpdate), medan omdömet i den spårade .NET-instansen är 7. När SaveChanges anropas identifierar EF att det nya värdet 7 skiljer sig från det ursprungliga värdet 5 och bevarar den ändringen. Ändringen som utförts av ExecuteUpdate skrivs över och tas inte i beaktande.

Därför är det vanligtvis en bra idé att undvika att blanda både spårade SaveChanges ändringar och ospårade ändringar viaExecuteUpdate/ExecuteDelete .

Transaktioner

Om du fortsätter med ovanstående är det viktigt att förstå att ExecuteUpdate och ExecuteDelete inte startar implicit en transaktion när de anropas. Överväg följande kod:

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();

Varje ExecuteUpdate anrop gör att en enskild SQL UPDATE skickas till databasen. Eftersom ingen transaktion skapas, om något slags fel hindrar den andra ExecuteUpdate från att slutföras korrekt, sparas effekterna av den första fortfarande i databasen. I själva verket körs de fyra åtgärderna ovan – två anrop av ExecuteUpdate, en fråga och SaveChanges – var och en i sin egen transaktion. Om du vill omsluta flera åtgärder i en enda transaktion startar du uttryckligen en transaktion med DatabaseFacade:

using (var transaction = context.Database.BeginTransaction())
{
    context.Blogs.ExecuteUpdate(/* some update */);
    context.Blogs.ExecuteUpdate(/* another update */);

    ...
}

Mer information om transaktionshantering finns i Använda transaktioner.

Samtidighetskontroll och rader som påverkas

SaveChanges tillhandahåller automatisk samtidighetskontroll med hjälp av en samtidighetstoken för att säkerställa att en rad inte har ändrats mellan det ögonblick då du läste in den och när du sparar ändringar i den. Eftersom ExecuteUpdate och ExecuteDelete inte interagerar med ändringsspåraren kan de inte automatiskt tillämpa samtidighetskontroll.

Båda dessa metoder returnerar dock antalet rader som påverkades av åtgärden. Detta kan vara särskilt praktiskt för att implementera samtidighetskontroll själv:

// (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!");
}

I den här koden använder vi en LINQ-operator Where för att tillämpa en uppdatering på en specifik blogg, och endast om dess samtidighetstoken har ett specifikt värde (t.ex. den vi såg när vi frågar bloggen från databasen). Sedan kontrollerar vi hur många rader som faktiskt uppdaterades av ExecuteUpdate. Om resultatet är noll uppdaterades inga rader och samtidighetstoken ändrades troligen till följd av en samtidig uppdatering.

Begränsningar

  • För närvarande stöds endast uppdatering och borttagning. infogning måste göras via DbSet<TEntity>.Add och SaveChanges().
  • Även om SQL UPDATE- och DELETE-uttrycken tillåter hämtning av ursprungliga kolumnvärden för de rader som påverkas, stöds detta för närvarande inte av ExecuteUpdate och ExecuteDelete.
  • Det går inte att batcha flera anrop av dessa metoder. Varje anrop utför sin egen tur och retur till databasen.
  • Databaser tillåter vanligtvis att endast en enskild tabell ändras med UPDATE eller DELETE.
  • Dessa metoder fungerar för närvarande endast med relationsdatabasprovidrar.

Ytterligare resurser