ExecuteUpdate und ExecuteDelete

Hinweis

Dieses Feature wurde in EF Core 7.0 eingeführt.

ExecuteUpdate und ExecuteDelete sind eine Möglichkeit, Daten in der Datenbank zu speichern, ohne die herkömmliche Änderungsnachverfolgung und SaveChanges()-Methode von EF zu verwenden. Einen einführenden Vergleich dieser beiden Techniken finden Sie auf der Übersichtsseite zum Speichern von Daten.

ExecuteDelete

Angenommen, Sie müssen alle Blogs löschen, deren Bewertung unter einem bestimmten Schwellenwert liegt. Der herkömmliche SaveChanges()-Ansatz erfordert Folgendes:

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

context.SaveChanges();

Dies ist eine ziemlich ineffiziente Methode, um diese Aufgabe auszuführen: Wir fragen die Datenbank nach allen Blogs ab, die unserem Filter entsprechen, und dann fragen wir alle diese Instanzen ab, materialisieren sie und verfolgen sie nach. Die Anzahl der übereinstimmenden Entitäten könnte sehr groß sein. Anschließend teilen wir der Änderungsnachverfolgung von EF mit, dass jeder Blog entfernt werden muss, und wenden diese Änderungen an, indem wir SaveChanges() aufrufen, wodurch eine DELETE-Anweisung für jeden einzelnen von ihnen generiert wird.

Hier wird die gleiche Aufgabe über die ExecuteDelete-API ausgeführt:

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

Dabei werden die vertrauten LINQ-Operatoren verwendet, um zu ermitteln, welche Blogs betroffen sein sollten – genau wie bei einer Abfrage – und dann wird EF aufgefordert, eine SQL-DELETE-Instanz für die Datenbank auszuführen:

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

Abgesehen davon, dass dies einfacher und kürzer ist, wird dies sehr effizient in der Datenbank ausgeführt, ohne Daten aus der Datenbank zu laden oder die Änderungsnachverfolgung von EF einzubeziehen. Beachten Sie, dass Sie beliebige LINQ-Operatoren verwenden können, um auszuwählen, welche Blogs Sie löschen möchten. Diese werden zur Ausführung in der Datenbank in SQL übersetzt, so als ob Sie diese Blogs abfragen würden.

ExecuteUpdate

Was wäre, wenn wir diese Blogs nicht löschen, sondern stattdessen eine Eigenschaft ändern würden, um anzugeben, dass sie ausgeblendet werden sollen? ExecuteUpdate bietet eine ähnliche Möglichkeit zum Ausdrücken einer SQL-UPDATE-Anweisung:

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

Wie bei ExecuteDelete verwenden wir zunächst LINQ, um zu bestimmen, welche Blogs betroffen sein sollen. Mit ExecuteUpdate müssen wir aber auch die Änderung ausdrücken, die auf die übereinstimmenden Blogs angewendet werden soll. Hierzu rufen Sie innerhalb des ExecuteUpdate-Aufrufs SetProperty auf und geben zwei Argumente an: die zu ändernde Eigenschaft (IsVisible) und den neuen Wert, den sie haben soll (false). Dadurch wird die folgende SQL-Instanz ausgeführt:

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

Aktualisieren mehrerer Eigenschaften

ExecuteUpdate ermöglicht das Aktualisieren mehrerer Eigenschaften in einem einzigen Aufruf. Wenn Sie beispielsweise IsVisible auf „false“ und Rating auf „null“ (0) festlegen möchten, verketten Sie einfach zusätzliche SetProperty-Aufrufe:

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

Dadurch wird der folgende SQL-Code ausgeführt:

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

Verweisen auf den vorhandenen Eigenschaftswert

In den obigen Beispielen wurde die Eigenschaft auf einen neuen konstanten Wert aktualisiert. ExecuteUpdate ermöglicht auch den Verweis auf den vorhandenen Eigenschaftswert bei der Berechnung des neuen Werts. Verwenden Sie beispielsweise Folgendes, um die Bewertung aller übereinstimmenden Blogs um eins zu erhöhen:

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

Beachten Sie, dass das zweite Argument von SetProperty jetzt eine Lambdafunktion und nicht wie zuvor eine Konstante ist. Ihr b-Parameter stellt den Blog dar, der aktualisiert wird. Innerhalb dieser Lambdafunktion enthält b.Rating also die Bewertung, bevor eine Änderung aufgetreten ist. Dadurch wird der folgende SQL-Code ausgeführt:

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

ExecuteUpdate unterstützt derzeit nicht das Verweisen auf Navigationen innerhalb der SetProperty-Lambdafunktion. Angenommen, wir möchten alle Bewertungen der Blogs aktualisieren, sodass die neue Bewertung jedes Blogs dem Durchschnitt aller Bewertungen der Beiträge entspricht. Wir können versuchen, ExecuteUpdate wie folgt zu verwenden:

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

EF ermöglicht jedoch die Durchführung dieses Vorgangs, indem zuerst mit Select die durchschnittliche Bewertung berechnet und auf einen anonymen Typ projiziert und dann ExecuteUpdate darüber verwendet wird:

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

Dadurch wird der folgende SQL-Code ausgeführt:

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]

Änderungsnachverfolgung

Mit SaveChanges vertraute Benutzer*innen sind daran gewöhnt, mehrere Änderungen durchzuführen, und dann SaveChanges aufzurufen, um alle diese Änderungen auf die Datenbank anzuwenden. Dies macht die Änderungsnachverfolgung von EF möglich, die diese Änderungen akkumuliert bzw. nachverfolgt.

ExecuteUpdate und ExecuteDelete funktionieren ganz anders: Sie werden sofort an dem Punkt wirksam, an dem sie aufgerufen werden. Dies bedeutet, dass ein einzelner ExecuteUpdate- oder ExecuteDelete-Vorgang zwar viele Zeilen betreffen kann, es jedoch nicht möglich ist, mehrere solcher Vorgänge zu akkumulieren und gleichzeitig anzuwenden, z. B. beim Aufrufen von SaveChanges. Tatsächlich wissen die Funktionen absolut nichts von der Änderungsnachverfolgung von EF und haben mit ihr keinerlei Interaktion. Dies hat mehrere wichtige Konsequenzen.

Betrachten Sie folgenden 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 = 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();

Entscheidend ist, dass die Änderungsnachverfolgung von EF nicht aktualisiert wird, wenn ExecuteUpdate aufgerufen wird und alle Blogs in der Datenbank aktualisiert werden, und die nachverfolgte .NET-Instanz immer noch ihren ursprünglichen Bewertungswert von dem Zeitpunkt hat, zu dem sie abgefragt wurde. Angenommen, die Bewertung des Blogs war ursprünglich 5. Nachdem die 3. Zeile ausgeführt wurde, ist die Bewertung in der Datenbank jetzt 6 (aufgrund von ExecuteUpdate), während die Bewertung in der nachverfolgten .NET-Instanz 7 ist. Wenn SaveChanges aufgerufen wird, erkennt EF, dass sich der neue Wert 7 vom ursprünglichen Wert 5 unterscheidet, und behält diese Änderung bei. Die von ExecuteUpdate vorgenommene Änderung wird überschrieben und nicht berücksichtigt.

Daher ist es in der Regel eine gute Idee, sowohl nachverfolgte SaveChanges-Änderungen als auch nicht nachverfolgte Änderungen über ExecuteUpdate/ExecuteDelete zu mischen.

Transaktionen

Wenn Sie mit dem obigen Vorgang fortfahren, müssen Sie unbedingt verstehen, dass ExecuteUpdate und ExecuteDelete nicht implizit eine Transaktion starten, wenn sie aufgerufen werden. Betrachten Sie folgenden Code:

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

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

Jeder ExecuteUpdate-Aufruf bewirkt, dass ein einzelnes SQL-UPDATE an die Datenbank gesendet wird. Da keine Transaktion erstellt wird, wenn ein Fehler verhindert, dass das zweite ExecuteUpdate erfolgreich abgeschlossen wird, werden die Auswirkungen des ersten weiterhin in der Datenbank beibehalten. Tatsächlich werden die vier oben genannten Vorgänge – zwei Aufrufe von ExecuteUpdate, eine Abfrage und SaveChanges – jeweils innerhalb ihrer eigenen Transaktion ausgeführt. Um mehrere Vorgänge in einer einzelnen Transaktion zu umschließen, starten Sie eine Transaktion explizit mit DatabaseFacade:

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

    ...
}

Weitere Informationen zur Transaktionsverarbeitung finden Sie unter Verwenden von Transaktionen.

Parallelitätssteuerung und betroffene Zeilen

SaveChanges stellt die automatische Parallelitätssteuerung mithilfe eines Parallelitätstokens bereit, um sicherzustellen, dass eine Zeile nicht zwischen dem Zeitpunkt des Ladens und dem Zeitpunkt, zu dem Sie Änderungen speichern, geändert wurde. Da ExecuteUpdate und ExecuteDelete nicht mit der Änderungsnachverfolgung interagieren, können sie die Parallelitätssteuerung nicht automatisch anwenden.

Beide Methoden geben jedoch die Anzahl der Zeilen zurück, die vom Vorgang betroffen waren. Dies kann besonders hilfreich sein, um die Parallelitätssteuerung selbst zu implementieren:

// (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 diesem Code verwenden wir einen LINQ-Where-Operator, um ein Update auf einen bestimmten Blog anzuwenden, und zwar nur, wenn das Parallelitätstoken einen bestimmten Wert aufweist (z. B. den, den wir beim Abfragen des Blogs aus der Datenbank gesehen haben). Wir überprüfen anschließend, wie viele Zeilen tatsächlich von ExecuteUpdate aktualisiert wurden. Wenn das Ergebnis null (0) ist, wurden keine Zeilen aktualisiert, und das Parallelitätstoken wurde wahrscheinlich als Ergebnis einer gleichzeitigen Aktualisierung geändert.

Begrenzungen

  • Derzeit wird nur das Aktualisieren und Löschen unterstützt. Das Einfügen muss über DbSet<TEntity>.Add und SaveChanges() erfolgen.
  • Während die SQL UPDATE- und DELETE-Anweisungen das Abrufen der ursprünglichen Spaltenwerte für die betroffenen Zeilen zulassen, wird dies derzeit nicht von ExecuteUpdate und ExecuteDelete unterstützt.
  • Mehrere Aufrufe dieser Methoden können nicht gestapelt werden. Jeder Aufruf führt einen eigenen Roundtrip zur Datenbank durch.
  • Datenbanken lassen in der Regel nur zu, eine einzelne Tabelle mit UPDATE oder DELETE zu ändern.
  • Diese Methoden funktionieren derzeit nur mit relationalen Datenbankanbietern.

Zusätzliche Ressourcen