ExecuteUpdate 和 ExecuteDelete

备注

EF Core 7.0 中已引入此功能。

ExecuteUpdateExecuteDelete 是一种将数据保存到数据库的方法,无需使用 EF 的传统更改跟踪和 SaveChanges() 方法。 有关这两种方法的介绍性比较,请参阅有关保存数据的概述页

ExecuteDelete

假设需要删除评分低于特定阈值的所有博客。 传统 SaveChanges() 方法要求执行以下操作:

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

context.SaveChanges();

这是执行此任务的低效方法:我们在数据库中查询与筛选器匹配的所有博客,然后查询、具体化和跟踪所有这些实例;匹配实体的数量可能很大。 然后,我们告诉 EF 的更改跟踪器,需要删除每个博客,并通过调用 SaveChanges() 来应用这些更改,这会为每个博客生成 DELETE 语句。

下面是通过 ExecuteDelete API 执行的相同任务:

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

这会使用熟悉的 LINQ 运算符来确定哪些博客应受到影响(就像我们在查询它们一样),然后告诉 EF 针对数据库执行 SQL DELETE

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

除了更简单、更短外,还会在数据库中非常高效地执行,无需从数据库加载任何数据或涉及 EF 的更改跟踪器。 请注意,可以使用任意 LINQ 运算符来选择要删除的博客 - 这些博客将转换为 SQL 以在数据库中执行,就像查询这些博客一样。

ExecuteUpdate

如果我们想要更改属性以指示应隐藏这些博客,而不是将其删除,该怎么办? ExecuteUpdate 提供了一种类似的方法来表达 SQL UPDATE 语句:

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

ExecuteDelete 一样,我们首先使用 LINQ 来确定应受到影响的博客;但对于 ExecuteUpdate,我们还需要表达要应用于匹配博客的更改。 这是通过在 ExecuteUpdate 调用中调用 SetProperty,并为其提供两个参数来完成的:要更改的属性 (IsVisible),以及它应具有的新值 (false)。 这会导致执行以下 SQL:

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

更新多个属性

ExecuteUpdate 允许在单个调用中更新多个属性。 例如,若要将 IsVisible 设置为 false 并将 Rating 设置为零,只需将其他 SetProperty 调用链接在一起:

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

这会执行以下 SQL:

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

引用现有属性值

上述示例将属性更新为新的常量值。 ExecuteUpdate 还允许在计算新值时引用现有属性值;例如,若要将所有匹配博客的评分提高一分,请使用以下内容:

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:

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

ExecuteUpdate 当前不支持在 SetProperty lambda 中引用导航。 例如,假设我们要更新所有博客的评分,以便每个博客的新评分是其所有帖子评分的平均值。 我们可能会尝试使用 ExecuteUpdate,如下所示:

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

但是,EF 确实允许执行此操作,方法是先使用 Select 计算平均评分并将其投影为匿名类型,然后针对该类型使用 ExecuteUpdate

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:

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 的更改跟踪器实现,该跟踪器会累积或跟踪这些更改。

ExecuteUpdateExecuteDelete 的工作方式大不相同:它们在调用时立即生效。 这意味着,虽然单个 ExecuteUpdateExecuteDelete 操作可能会影响许多行,但无法累积多个此类操作并同时应用它们,例如在调用 SaveChanges 时。 事实上,这些函数完全不知道 EF 的更改跟踪器,并且没有任何交互。 这可产生多个重要的结果。

考虑下列代码:

// 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 修改和未跟踪的修改。

事务

继续上述内容,请务必了解 ExecuteUpdateExecuteDelete 不会隐式启动调用它们的事务。 考虑下列代码:

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 显式启动事务:

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

    ...
}

有关事务处理的详细信息,请参阅使用事务

并发控制和受影响的行

SaveChanges 提供自动并发控制,使用并发令牌确保在加载行到保存对该行的更改的一段时间内不会更改行。 由于 ExecuteUpdateExecuteDelete 不与更改跟踪器交互,因此它们无法自动应用并发控制。

但是,这两种方法都返回受操作影响的行数;这对于自行实现并发控制特别有用:

// (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>.AddSaveChanges() 完成插入。
  • 虽然 SQL UPDATE 和 DELETE 语句允许检索受影响行的原始列值,但 ExecuteUpdateExecuteDelete 目前不支持此操作。
  • 这些方法的多次调用无法进行批处理。 每次调用都会对数据库执行自己的往返。
  • 数据库通常只允许使用 UPDATE 或 DELETE 修改单个表。
  • 这些方法目前仅适用于关系数据库提供程序。

其他资源