EFCore中ExecuteUpdate 和 ExecuteDelete

一个人走在路上發表於2024-03-30

ExecuteUpdate 和 ExecuteDelete
專案
2023/05/11
4 個參與者

反饋
本文內容
ExecuteDelete
ExecuteUpdate
Change tracking
事務
顯示另外 3 個
備註

EF Core 7.0 中已引入此功能。

ExecuteUpdate 和 ExecuteDelete 是一種將資料儲存到資料庫的方法,無需使用 EF 的傳統更改跟蹤和 SaveChanges() 方法。 有關這兩種方法的介紹性比較,請參閱有關儲存資料的概述頁。

ExecuteDelete
假設需要刪除評分低於特定閾值的所有部落格。 傳統 SaveChanges() 方法要求執行以下操作:

c#

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

context.SaveChanges();
這是執行此任務的低效方法:我們在資料庫中查詢與篩選器匹配的所有部落格,然後查詢、具體化和跟蹤所有這些例項;匹配實體的數量可能很大。 然後,我們告訴 EF 的更改跟蹤器,需要刪除每個部落格,並透過呼叫 SaveChanges() 來應用這些更改,這會為每個部落格生成 DELETE 語句。

下面是透過 ExecuteDelete API 執行的相同任務:

c#

複製
context.Blogs.Where(b => b.Rating < 3).ExecuteDelete();
這會使用熟悉的 LINQ 運算子來確定哪些部落格應受到影響(就像我們在查詢它們一樣),然後告訴 EF 針對資料庫執行 SQL DELETE:

SQL

複製
DELETE FROM [b]
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
除了更簡單、更短外,還會在資料庫中非常高效地執行,無需從資料庫載入任何資料或涉及 EF 的更改跟蹤器。 請注意,可以使用任意 LINQ 運算子來選擇要刪除的部落格 - 這些部落格將轉換為 SQL 以在資料庫中執行,就像查詢這些部落格一樣。

ExecuteUpdate
如果我們想要更改屬性以指示應隱藏這些部落格,而不是將其刪除,該怎麼辦? ExecuteUpdate 提供了一種類似的方法來表達 SQL UPDATE 語句:

c#

複製
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.IsVisible, false));
與 ExecuteDelete 一樣,我們首先使用 LINQ 來確定應受到影響的部落格;但對於 ExecuteUpdate,我們還需要表達要應用於匹配部落格的更改。 這是透過在 ExecuteUpdate 呼叫中呼叫 SetProperty,併為其提供兩個引數來完成的:要更改的屬性 (IsVisible),以及它應具有的新值 (false)。 這會導致執行以下 SQL:

SQL

複製
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
更新多個屬性
ExecuteUpdate 允許在單個呼叫中更新多個屬性。 例如,若要將 IsVisible 設定為 false 並將 Rating 設定為零,只需將其他 SetProperty 呼叫連結在一起:

c#

複製
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters
.SetProperty(b => b.IsVisible, false)
.SetProperty(b => b.Rating, 0));
這會執行以下 SQL:

SQL

複製
UPDATE [b]
SET [b].[Rating] = 0,
[b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
引用現有屬性值
上述示例將屬性更新為新的常量值。 ExecuteUpdate 還允許在計算新值時引用現有屬性值;例如,若要將所有匹配部落格的評分提高一分,請使用以下內容:

c#

複製
context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdate(setters => setters.SetProperty(b => b.Rating, b => b.Rating + 1));
請注意,SetProperty 的第二個引數現在是 lambda 函式,而不是之前的常量。 其 b 參數列示正在更新的部落格;因此,在該 lambda 中,b.Rating 包含發生任何更改前的評分。 這會執行以下 SQL:

SQL

複製
UPDATE [b]
SET [b].[Rating] = [b].[Rating] + 1
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
導航和相關實體
ExecuteUpdate 當前不支援在 SetProperty lambda 中引用導航。 例如,假設我們要更新所有部落格的評分,以便每個部落格的新評分是其所有帖子評分的平均值。 我們可能會嘗試使用 ExecuteUpdate,如下所示:

c#

複製
context.Blogs.ExecuteUpdate(
setters => setters.SetProperty(b => b.Rating, b => b.Posts.Average(p => p.Rating)));
但是,EF 確實允許執行此操作,方法是先使用 Select 計算平均評分並將其投影為匿名型別,然後針對該型別使用 ExecuteUpdate:

c#

複製
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));
這會執行以下 SQL:

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]
Change tracking
熟悉 SaveChanges 的使用者習慣於執行多個更改,然後呼叫 SaveChanges 以將所有這些更改應用於資料庫;這可以透過 EF 的更改跟蹤器實現,該跟蹤器會累積或跟蹤這些更改。

ExecuteUpdate 和 ExecuteDelete 的工作方式大不相同:它們在呼叫時立即生效。 這意味著,雖然單個 ExecuteUpdate 或 ExecuteDelete 操作可能會影響許多行,但無法累積多個此類操作並同時應用它們,例如在呼叫 SaveChanges 時。 事實上,這些函式完全不知道 EF 的更改跟蹤器,並且沒有任何互動。 這可產生多個重要的結果。

考慮下列程式碼:

c#

複製
// 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();
非常重要的一點是,當呼叫 ExecuteUpdate 且在資料庫中更新所有部落格時,EF 的更改跟蹤器不會更新,並且跟蹤的 .NET 例項仍具有其原始評分值(從查詢時開始)。 假設部落格的評分最初為 5;執行第 3 行後,資料庫中的評分現在為 6(由於 ExecuteUpdate),而跟蹤的 .NET 例項中的評分為 7。 呼叫 SaveChanges 時,EF 檢測到新值 7 與原始值 5 不同,並保留該更改。 由 ExecuteUpdate 執行的更改將被覆蓋且不考慮在內。

因此,通常最好避免透過 ExecuteUpdate/ExecuteDelete 混合跟蹤的 SaveChanges 修改和未跟蹤的修改。

事務
繼續上述內容,請務必瞭解 ExecuteUpdate 和 ExecuteDelete 不會隱式啟動呼叫它們的事務。 考慮下列程式碼:

c#

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

var blog = context.Blogs.Single(b => b.Name == "SomeBlog");
blog.Rating += 2;
context.SaveChanges();
每次 ExecuteUpdate 呼叫都會將單個 SQL UPDATE 傳送到資料庫。 由於未建立事務,因此,如果任何型別的失敗阻止第二個 ExecuteUpdate 成功完成,則第一個操作的影響仍會儲存到資料庫中。 事實上,上述四個操作(ExecuteUpdate 的兩次呼叫、一個查詢和 SaveChanges)各自在其自己的事務中執行。 若要在單個事務中包裝多個操作,請使用 DatabaseFacade 顯式啟動事務:

c#

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

...
}
有關事務處理的詳細資訊,請參閱使用事務。

併發控制和受影響的行
SaveChanges 提供自動併發控制,使用併發令牌確保在載入行到儲存對該行的更改的一段時間內不會更改行。 由於 ExecuteUpdate 和 ExecuteDelete 不與更改跟蹤器互動,因此它們無法自動應用併發控制。

但是,這兩種方法都返回受操作影響的行數;這對於自行實現併發控制特別有用:

c#

複製
// (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!");
}
在此程式碼中,我們使用 LINQ Where 運算子將更新應用於特定部落格,並且僅當其併發令牌具有特定值(例如,從資料庫查詢部落格時看到的值)時應用。 我們檢查 ExecuteUpdate 實際更新了多少行;如果結果為零,則不更新任何行,併發令牌可能會因併發更新而更改。

限制
目前僅支援更新和刪除;必須透過 DbSet<TEntity>.Add 和 SaveChanges() 完成插入。
雖然 SQL UPDATE 和 DELETE 語句允許檢索受影響行的原始列值,但 ExecuteUpdate 和 ExecuteDelete 目前不支援此操作。
這些方法的多次呼叫無法進行批處理。 每次呼叫都會對資料庫執行自己的往返。
資料庫通常只允許使用 UPDATE 或 DELETE 修改單個表。
這些方法目前僅適用於關聯式資料庫提供程式。
其他資源

相關文章