Delen via


"ExecuteUpdate" en "ExecuteDelete"

ExecuteUpdate en ExecuteDelete zijn een manier om gegevens op te slaan in de database zonder gebruik te maken van de traditionele wijzigingentracking en SaveChanges() -methode van EF. Zie de overzichtspagina over het opslaan van gegevens voor een inleidende vergelijking van deze twee technieken.

ExecuteDelete

Stel dat u alle blogs met een classificatie onder een bepaalde drempelwaarde moet verwijderen. Voor de traditionele SaveChanges() benadering moet u het volgende doen:

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

await context.SaveChangesAsync();

Dit is een behoorlijk inefficiënte manier om deze taak uit te voeren: we voeren een query uit op de database voor alle blogs die overeenkomen met ons filter, en vervolgens zoeken, materialiseren en bijhouden van al deze exemplaren; het aantal overeenkomende entiteiten kan enorm zijn. Vervolgens vertellen we EF's wijzigingentracker dat elke blog moet worden verwijderd en passen we de wijzigingen toe door SaveChanges() aan te roepen, waarmee een DELETE commando voor elk van deze blogs wordt gegenereerd.

Dit is dezelfde taak die wordt uitgevoerd via de ExecuteDelete API:

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

Dit maakt gebruik van de bekende LINQ-operators om te bepalen welke Blogs moeten worden aangepakt - alsof we ze zouden opvragen - en vraagt EF vervolgens om een SQL DELETE uit te voeren op de database.

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

Afgezien van eenvoudiger en korter, wordt dit zeer efficiënt uitgevoerd in de database, zonder gegevens uit de database te laden of de wijzigingentracker van EF te betrekken. Houd er rekening mee dat u willekeurige LINQ-operators kunt gebruiken om te selecteren welke blogs u wilt verwijderen. Deze worden vertaald naar SQL voor uitvoering in de database, net zoals wanneer u die blogs opvraagt.

ExecuteUpdate

In plaats van deze blogs te verwijderen, wat als we een eigenschap willen wijzigen om aan te geven dat ze in plaats daarvan moeten worden verborgen? ExecuteUpdate biedt een vergelijkbare manier om een SQL-instructie UPDATE uit te drukken:

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

Net als bij ExecuteDelete, gebruiken we EERST LINQ om te bepalen welke blogs moeten worden beïnvloed; maar met ExecuteUpdate we moeten ook de wijziging uitdrukken die moet worden toegepast op de overeenkomende blogs. Dit wordt gedaan door het aanroepen van SetProperty binnen de ExecuteUpdate aanroep, en het op te geven met twee argumenten: de eigenschap die moet worden gewijzigd (IsVisible), en de nieuwe waarde die het moet hebben (false). Dit zorgt ervoor dat de volgende SQL wordt uitgevoerd:

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

Meerdere eigenschappen bijwerken

ExecuteUpdate staat het bijwerken van meerdere eigenschappen in één aanroep toe. Als u bijvoorbeeld zowel IsVisible op onwaar als Rating op nul wilt instellen, koppelt u gewoon extra SetProperty-aanroepen.

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

Hiermee wordt de volgende SQL uitgevoerd:

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

Verwijzen naar de bestaande eigenschapswaarde

In de bovenstaande voorbeelden is de eigenschap bijgewerkt naar een nieuwe constante waarde. ExecuteUpdate kan ook verwijzen naar de bestaande eigenschapswaarde bij het berekenen van de nieuwe waarde; Als u bijvoorbeeld de waardering van alle overeenkomende blogs met één wilt verhogen, gebruikt u het volgende:

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

Houd er rekening mee dat het tweede argument SetProperty nu een lambda-functie is en niet een constante als voorheen. De b parameter vertegenwoordigt de blog die wordt bijgewerkt; binnen die lambda, b.Rating bevat dus de classificatie voordat er een wijziging is opgetreden. Hiermee wordt de volgende SQL uitgevoerd:

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

ExecuteUpdate ondersteunt momenteel geen verwijzingen naar navigatie in de SetProperty lambda. Stel dat we alle blogs willen bijwerken, zodat de nieuwe classificatie van elke blog het gemiddelde is van de beoordelingen van alle berichten. We kunnen proberen om als volgt te gebruiken ExecuteUpdate :

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

EF biedt echter wel de mogelijkheid om deze bewerking eerst uit te voeren door Select de gemiddelde waardering te berekenen en deze te projecteren op een anoniem type, en vervolgens het volgende te gebruiken ExecuteUpdate :

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

Hiermee wordt de volgende SQL uitgevoerd:

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]

Wijzigingen bijhouden

Gebruikers die vertrouwd zijn met SaveChanges zijn gewend meerdere wijzigingen aan te brengen en vervolgens SaveChanges aan te roepen om al deze wijzigingen op de database toe te passen. Dit wordt mogelijk gemaakt door de wijzigingentracker van EF, die deze wijzigingen verzamelt of bijhoudt.

ExecuteUpdate en ExecuteDelete werken heel anders: ze worden onmiddellijk van kracht, op het punt waarop ze worden aangeroepen. Dit betekent dat hoewel één ExecuteUpdate of ExecuteDelete bewerking veel rijen kan beïnvloeden, het niet mogelijk is om meerdere dergelijke bewerkingen te verzamelen en ze tegelijk toe te passen, bijvoorbeeld bij het aanroepen SaveChanges. In feite zijn de functies helemaal niet op de hoogte van de wijzigingentracker van EF en hebben er helemaal geen interactie mee. Dit heeft verschillende belangrijke gevolgen.

Houd rekening met de volgende code:

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

Cruciaal is dat wanneer ExecuteUpdate wordt aangeroepen en alle blogs in de database worden bijgewerkt, de wijzigingstracker van EF niet wordt bijgewerkt en het bijgehouden .NET-exemplaar nog steeds de oorspronkelijke classificatiewaarde heeft, vanaf het moment waarop deze is opgevraagd. Stel dat de beoordeling van de blog oorspronkelijk 5 was; nadat de 3e regel is uitgevoerd, is de classificatie in de database nu 6 (vanwege de ExecuteUpdate), terwijl de classificatie in het bijgehouden .NET-exemplaar 7 is. Wanneer SaveChanges wordt aangeroepen, detecteert EF dat de nieuwe waarde 7 verschilt van de oorspronkelijke waarde 5 en blijft die wijziging behouden. De wijziging die is uitgevoerd door ExecuteUpdate wordt overschreven en niet in aanmerking genomen.

Als gevolg hiervan is het meestal een goed idee om te vermijden dat zowel de bijgehouden SaveChanges wijzigingen als de niet-bijgehouden wijzigingen via ExecuteUpdate/ExecuteDelete worden gemengd.

Transacties

Voortbordurend op het bovenstaande, is het belangrijk te begrijpen dat ExecuteUpdate en ExecuteDelete niet impliciet een transactie starten wanneer ze worden aangeroepen. Houd rekening met de volgende code:

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

Elke ExecuteUpdate aanroep zorgt ervoor dat één SQL UPDATE naar de database wordt verzonden. Omdat er geen transactie wordt gemaakt, worden de effecten van de eerste transactie nog steeds opgeslagen in de database als een of meer fouten verhinderen dat de tweede ExecuteUpdate fout wordt voltooid. De vier bovenstaande bewerkingen : twee aanroepen van ExecuteUpdate, een query en SaveChanges - elk worden uitgevoerd binnen hun eigen transactie. Als u meerdere bewerkingen in één transactie wilt verpakken, start u expliciet een transactie met DatabaseFacade:

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

    ...
}

Zie Transacties gebruiken voor meer informatie over transactieafhandeling.

Invloed op gelijktijdigheidsbeheer en rijen

SaveChanges biedt automatisch gelijktijdigheidsbeheer, waarbij u een gelijktijdigheidstoken gebruikt om ervoor te zorgen dat een rij niet is gewijzigd tussen het moment dat u deze hebt geladen en het moment waarop u wijzigingen opslaat. Omdat ExecuteUpdate en ExecuteDelete geen interactie hebben met de wijzigingstracker, kunnen ze niet automatisch gelijktijdigheidsbeheer toepassen.

Beide methoden retourneren echter het aantal rijen dat door de bewerking is beïnvloed; Dit kan met name handig zijn voor het zelf implementeren van gelijktijdigheidsbeheer:

// (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 deze code gebruiken we een LINQ-operator Where om een update toe te passen op een specifiek blog en alleen als het gelijktijdigheidstoken een specifieke waarde heeft (bijvoorbeeld de waarde die we hebben gezien bij het uitvoeren van een query op de blog uit de database). Vervolgens controleren we hoeveel rijen daadwerkelijk zijn bijgewerkt door; als het resultaat nul is, zijn er geen rijen bijgewerkt ExecuteUpdateen is het gelijktijdigheidstoken waarschijnlijk gewijzigd als gevolg van een gelijktijdige update.

Beperkingen

  • Alleen het bijwerken en verwijderen wordt momenteel ondersteund; invoeging moet worden uitgevoerd via DbSet<TEntity>.Add en SaveChanges().
  • Hoewel de SQL UPDATE- en DELETE-instructies het ophalen van oorspronkelijke kolomwaarden voor de betrokken rijen toestaan, wordt dit momenteel niet ondersteund door ExecuteUpdate en ExecuteDelete.
  • Meerdere aanroepen van deze methoden kunnen niet in batches worden uitgevoerd. Elke aanroep voert zijn eigen reis naar de database uit.
  • Databases staan doorgaans slechts één tabel toe om te worden gewijzigd met UPDATE of DELETE.
  • Deze methoden werken momenteel alleen met relationele databaseproviders.

Aanvullende bronnen