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. Der zugehörige Parameter b
stellt den Blog dar, der aktualisiert wird. Innerhalb dieses Lambdas enthält b.Rating
also die Bewertung, bevor eine Änderung vorgenommen wurde. Dadurch wird der folgende SQL-Code ausgeführt:
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Navigationen und verwandte Entitäten
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
undExecuteDelete
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
- .NET Data Access Community Standup-Sitzung, in der wir
ExecuteUpdate
undExecuteDelete
diskutieren.