EF7批量删除和批量更新

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://learn.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-7.0/whatsnew

ExecuteUpdate 和 ExecuteDelete (批量更新)

默认情况下,EF Core 跟踪对实体的更改,然后在调用其中一个SaveChanges方法时向数据库发送更新。 仅针对实际更改的属性和关系发送更改。 此外,跟踪的实体与发送到数据库的更改保持同步。 此机制是向数据库发送常规用途插入、更新和删除的高效便捷方法。 这些更改也会进行批处理以减少数据库往返次数。

但是,有时在不涉及更改跟踪器的情况下对数据库执行更新或删除命令很有用。 EF7 使用新的 ExecuteUpdateExecuteDelete 方法启用此功能。 这些方法应用于 LINQ 查询,并将基于该查询的结果更新或删除数据库中的实体。 许多实体可以使用单个命令进行更新,并且实体不会加载到内存中,这意味着这可能会导致更高效的更新和删除。

但是,请记住:

  • 必须显式指定要进行的具体更改;EF Core 不会自动检测到它们。
  • 任何跟踪的实体都不会保持同步。
  • 可能需要按正确的顺序发送其他命令,以免违反数据库约束。 例如,在删除主体之前删除依赖项。

所有这些都意味着 ExecuteUpdateExecuteDelete 方法补充而不是取代现有 SaveChanges 机制。

基本 ExecuteDelete 示例

提示

此处显示的代码来自 ExecuteDeleteSample.cs

对 调用 ExecuteDeleteExecuteDeleteAsyncDbSet 会立即从数据库中删除该 DbSet 实体的所有实体。 例如,删除所有 Tag 实体:

await context.Tags.ExecuteDeleteAsync();

使用 SQL Server 时,这会执行以下 SQL:

DELETE FROM [t]
FROM [Tags] AS [t]

更有趣的是,查询可以包含筛选器。 例如:

await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();

这会执行以下 SQL:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'

查询还可以使用更复杂的筛选器,包括导航到其他类型的筛选器。 例如,仅从旧博客文章中删除标记:

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();

它执行:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

基本 ExecuteUpdate 示例

提示

此处显示的代码来自 ExecuteUpdateSample.cs

ExecuteUpdateExecuteUpdateAsync 的行为方式 ExecuteDelete 与 方法非常相似。 主要区别在于,更新需要知道要更新的属性以及如何更新它们。 这是使用对 的一个或多个调用实现的 SetProperty。 例如,更新 Name 每个博客的 :

await context.Blogs.ExecuteUpdateAsync(
    s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));

的第一个参数 SetProperty 指定要更新的属性;在本例中为 Blog.Name。 第二个参数指定应如何计算新值;在本例中,采用现有值并追加 "*Featured!*"。 生成的 SQL 为:

UPDATE [b]
    SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]

与 一 ExecuteDelete样,查询可用于筛选要更新的实体。 此外,对 的多次调用 SetProperty 可用于更新目标实体上的多个属性。 例如,若要更新 Title 2022 年之前发布的所有帖子的 和 Content

await context.Posts
    .Where(p => p.PublishedOn.Year < 2022)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
        .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));

在这种情况下,生成的 SQL 会稍微复杂一些:

UPDATE [p]
    SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
    [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022

最后,同样与 一 ExecuteDelete样,筛选器可以引用其他表。 例如,若要更新旧帖子中的所有标记,

await context.Tags
    .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

这将生成:

UPDATE [t]
    SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

继承和多个表

ExecuteUpdateExecuteDelete 只能对单个表执行操作。 在使用不同的 继承映射策略时,这会产生影响。 通常,使用 TPH 映射策略时没有问题,因为只有一个表要修改。 例如,删除所有 FeaturedPost 实体:

await context.Set<FeaturedPost>().ExecuteDeleteAsync();

使用 TPH 映射时生成以下 SQL:

DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'

使用 TPC 映射策略时,这种情况也没有问题,因为同样只需要对单个表进行更改:

DELETE FROM [f]
FROM [FeaturedPosts] AS [f]

但是,使用 TPT 映射策略时尝试此操作会失败,因为它需要从两个不同的表中删除行。

向查询添加筛选器通常意味着操作将失败,同时采用 TPC 和 TPT 策略。 这同样是因为可能需要从多个表中删除行。 例如下面的查询:

await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();

使用 TPH 时生成以下 SQL:

DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')

但使用 TPC 或 TPT 时失败。

ExecuteDelete 和 关系

如上所述,可能需要先删除或更新依赖实体,然后才能删除关系的主体。 例如,每个都 Post 依赖于其关联的 Author。 这意味着,如果帖子仍然引用作者,则无法删除该作者;这样做将违反数据库中的外键约束。 例如,尝试以下操作:

await context.Authors.ExecuteDeleteAsync();

将导致SQL Server出现以下异常:

Microsoft。Data.SqlClient.SqlException (0x80131904) :DELETE 语句与 REFERENCE 约束“FK_Posts_Authors_AuthorId”冲突。 该冲突发生在数据库“TphBlogsContext”表“dbo” 中。帖子“,列”AuthorId”。 语句已终止。

若要解决此问题,必须先删除帖子,或通过将外键属性设置为 AuthorId null 来断绝每个帖子与其作者之间的关系。 例如,使用 delete 选项:

await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();

提示

TagWith 可用于标记 ExecuteDeleteExecuteUpdate ,其方式与标记普通查询的方式相同。

这会导致两个单独的命令:第一个删除依赖项的 :

-- Deleting posts...

DELETE FROM [p]
FROM [Posts] AS [p]

删除主体的第二个:

-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]

重要

默认情况下,单个事务中不会包含多个 ExecuteDeleteExecuteUpdate 命令。 但是, DbContext 事务 API 可以正常使用,以在事务中包装这些命令。

在此处,在数据库中配置 级联删除 非常有用。 在我们的模型中,需要 和 Post 之间的关系Blog,这会导致 EF Core 按约定配置级联删除。 这意味着,当博客在数据库中被删除时,其所有依赖文章也将被删除。 然后,删除所有博客和文章,只需删除博客:

await context.Blogs.ExecuteDeleteAsync();

这会导致以下 SQL:

DELETE FROM [b]
FROM [Blogs] AS [b]

在删除博客时,它还会导致所有相关文章被配置的级联删除删除。