级联删除Cascade Delete

级联删除通常在数据库术语中用来描述一种允许在删除某行时自动触发删除相关行的特性。 EF Core 删除行为还介绍了一个密切相关的概念,即子实体与父实体的关系已断开时自动删除该子实体,这通常称为“删除孤立项”。

EF Core 实现多种不同的删除行为,并允许配置各个关系的删除行为。 EF Core 还实现基于关系的需求为每个关系自动配置有用的默认删除行为的约定。

删除行为Delete behaviors

删除行为在 DeleteBehavior 枚举器类型中定义,并且可以传递到 OnDelete Fluent API 来控制是主体/父实体的删除还是依赖实体/子实体关系的断开会对依赖实体/子实体产生副作用。

删除主体/父实体或断开与子实体的关系时有三个 EF 可执行的操作:

  • 可以删除子项/依赖项
  • 子项的外键值可以设置为 null
  • 子项保持不变

备注

仅当使用 EF Core 删除主体且将依赖实体加载到内存中(即对于跟踪的依赖项)时才应用 EF Core 模型中配置的删除行为。 需要在数据库中设置相应的级联行为以确保未由上下文跟踪的数据已应用必要的操作。 如果使用 EF Core 创建数据库,将为你设置此级联行为。

对于上述第二个操作,如果外键不可以为 null,则将外键值设置为 null 是无效的。 (不可为 null 的外键相当于必需关系。)在这些情况下,EF Core 会跟踪外键属性是否已被标记为 null,直到调用 SaveChanges,此时会引发异常,因为无法将更改永久保存到数据库中。 这类似于从数据库中获取约束冲突。

有四个删除行为,如下表中列出。

可选关系Optional relationships

对于可选关系(可以为 null 的外键),可以保存 null 外键值,从而产生以下影响:

行为名称对内存中的依赖项/子项的影响对数据库中的依赖项/子项的影响
Cascade删除实体删除实体
ClientSetNull(默认)外键属性设置为 nullNone
SetNull外键属性设置为 null外键属性设置为 null
RestrictNoneNone

必选关系Required relationships

对于必选关系(不可为 null 的外键),_不可以_保存 null 外键值,从而产生以下影响:

行为名称对内存中的依赖项/子项的影响对数据库中的依赖项/子项的影响
Cascade(默认)删除实体删除实体
ClientSetNullSaveChanges 引发异常None
SetNull引发 SaveChangesSaveChanges 引发异常
RestrictNoneNone

在上表中,“无” 可能会造成约束冲突。 例如,如果已删除主体/子实体,但不执行任何操作来更改依赖项/子项的外键,则由于发生外键约束冲突,数据库将可能会引发 SaveChanges。

高级别:

  • 如果实体在没有父项时不能存在,且希望 EF 负责自动删除子项,则使用“Cascade” 。
    • 在没有父项时不能存在的实体通常使用必选关系,其中“Cascade” 是默认值。
  • 如果实体可能有或可能没有父项,且希望 EF 负责为你将外键变为 null,则使用“ClientSetNull”
    • 在没有父项时可以存在的实体通常使用可选关系,其中“ClientSetNull” 是默认值。
    • 如果希望数据库即使在未加载子实体时也尝试将 null 值传播到子外键,则使用“SetNull” 。 但是,请注意,数据库必须支持此操作,并且如此配置数据库可能会导致其他限制,实际上这通常会使此选项不适用。 这就是SetNull不是默认值的原因。
  • 如果不希望 EF Core 始终自动删除实体或自动将外键变为 null,则使用“Restrict” 。 请注意,这要求使用代码手动同步子实体及其外键值,否则将引发约束异常。

备注

在 EF Core(与 EF6 不同)中,不会立即产生级联影响,而是仅在调用 SaveChanges 时产生。

备注

EF Core 2.0 中的更改: 在之前的版本中,“Restrict”将导致跟踪的依赖实体中的可选外键属性设置为 null,并且是可选关系的默认删除行为 。 在 EF Core 2.0 中,引入了“ClientSetNull” 以表示该行为,并且它会成为可选关系的默认值。 “Restrict” 的行为已调整为永远不会对依赖实体产生副作用。

实体删除示例Entity deletion examples

以下代码是可下载并运行的示例的一部分。 此示例显示了,当删除父实体时,可选关系和必选关系的每个删除行为会发生的情况。

  1. var blog = context.Blogs.Include(b => b.Posts).First();
  2. var posts = blog.Posts.ToList();
  3. DumpEntities(" After loading entities:", context, blog, posts);
  4. context.Remove(blog);
  5. DumpEntities($" After deleting blog '{blog.BlogId}':", context, blog, posts);
  6. try
  7. {
  8. Console.WriteLine();
  9. Console.WriteLine(" Saving changes:");
  10. context.SaveChanges();
  11. DumpSql();
  12. DumpEntities(" After SaveChanges:", context, blog, posts);
  13. }
  14. catch (Exception e)
  15. {
  16. DumpSql();
  17. Console.WriteLine();
  18. Console.WriteLine($" SaveChanges threw {e.GetType().Name}: {(e is DbUpdateException ? e.InnerException.Message : e.Message)}");
  19. }

我们来看一看每个变化以了解所发生的情况。

具有必选或可选关系的 DeleteBehavior.CascadeDeleteBehavior.Cascade with required or optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After deleting blog '1':
  6. Blog '1' is in state Deleted with 2 posts referenced.
  7. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  8. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  9. Saving changes:
  10. DELETE FROM [Posts] WHERE [PostId] = 1
  11. DELETE FROM [Posts] WHERE [PostId] = 2
  12. DELETE FROM [Blogs] WHERE [BlogId] = 1
  13. After SaveChanges:
  14. Blog '1' is in state Detached with 2 posts referenced.
  15. Post '1' is in state Detached with FK '1' and no reference to a blog.
  16. Post '2' is in state Detached with FK '1' and no reference to a blog.
  • 博客标记为已删除
  • 文章最初保持不变,因为在调用 SaveChanges 之前不会发生级联
  • SaveChanges 发送对依赖项/子项(文章)和主体/父项(博客)的删除
  • 保存后,所有实体都会分离,因为它们现在已从数据库中删除

具有必选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNullDeleteBehavior.ClientSetNull or DeleteBehavior.SetNull with required relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After deleting blog '1':
  6. Blog '1' is in state Deleted with 2 posts referenced.
  7. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  8. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  9. Saving changes:
  10. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 1
  11. SaveChanges threw DbUpdateException: Cannot insert the value NULL into column 'BlogId', table 'EFSaving.CascadeDelete.dbo.Posts'; column does not allow nulls. UPDATE fails. The statement has been terminated.
  • 博客标记为已删除
  • 文章最初保持不变,因为在调用 SaveChanges 之前不会发生级联
  • SaveChanges 尝试将文章外键设置为 null,但会失败,因为外键不可以为 null

具有可选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNullDeleteBehavior.ClientSetNull or DeleteBehavior.SetNull with optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After deleting blog '1':
  6. Blog '1' is in state Deleted with 2 posts referenced.
  7. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  8. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  9. Saving changes:
  10. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 1
  11. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 2
  12. DELETE FROM [Blogs] WHERE [BlogId] = 1
  13. After SaveChanges:
  14. Blog '1' is in state Detached with 2 posts referenced.
  15. Post '1' is in state Unchanged with FK 'null' and no reference to a blog.
  16. Post '2' is in state Unchanged with FK 'null' and no reference to a blog.
  • 博客标记为已删除
  • 文章最初保持不变,因为在调用 SaveChanges 之前不会发生级联
  • SaveChanges 尝试在删除主体/父项(博客)之前将依赖项/子项(文章)的外键设置为 null
  • 保存后,将删除主体/父项(博客),但仍会跟踪依赖项/子项(文章)
  • 跟踪的依赖项/子项(文章)现在具有 null 外键值,并且对删除的主体/父项(博客)的引用已删除

具有必选或可选关系的 DeleteBehavior.RestrictDeleteBehavior.Restrict with required or optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After deleting blog '1':
  6. Blog '1' is in state Deleted with 2 posts referenced.
  7. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  8. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  9. Saving changes:
  10. SaveChanges threw InvalidOperationException: The association between entity types 'Blog' and 'Post' has been severed but the foreign key for this relationship cannot be set to null. If the dependent entity should be deleted, then setup the relationship to use cascade deletes.
  • 博客标记为已删除
  • 文章最初保持不变,因为在调用 SaveChanges 之前不会发生级联
  • 由于Restrict指告知 EF 不要自动将外键设置为 null,因此它保持为非 null,而 SaveChanges 将引发异常但不进行保存

删除孤立项示例Delete orphans examples

以下代码是可下载并运行的示例的一部分。 此示例显示了,当断开父项/主体及其子项/依赖项之间的关系时,可选关系和必选关系的每个删除行为会发生的情况。 在此示例中,通过从主体/父项(博客)上的集合导航属性中删除依赖项/子项(文章)来断开关系。 但是,如果将从依赖项/子项到主体/父项的引用变为 null,则行为相同。

  1. var blog = context.Blogs.Include(b => b.Posts).First();
  2. var posts = blog.Posts.ToList();
  3. DumpEntities(" After loading entities:", context, blog, posts);
  4. blog.Posts.Clear();
  5. DumpEntities(" After making posts orphans:", context, blog, posts);
  6. try
  7. {
  8. Console.WriteLine();
  9. Console.WriteLine(" Saving changes:");
  10. context.SaveChanges();
  11. DumpSql();
  12. DumpEntities(" After SaveChanges:", context, blog, posts);
  13. }
  14. catch (Exception e)
  15. {
  16. DumpSql();
  17. Console.WriteLine();
  18. Console.WriteLine($" SaveChanges threw {e.GetType().Name}: {(e is DbUpdateException ? e.InnerException.Message : e.Message)}");
  19. }

我们来看一看每个变化以了解所发生的情况。

具有必选或可选关系的 DeleteBehavior.CascadeDeleteBehavior.Cascade with required or optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After making posts orphans:
  6. Blog '1' is in state Unchanged with 2 posts referenced.
  7. Post '1' is in state Modified with FK '1' and no reference to a blog.
  8. Post '2' is in state Modified with FK '1' and no reference to a blog.
  9. Saving changes:
  10. DELETE FROM [Posts] WHERE [PostId] = 1
  11. DELETE FROM [Posts] WHERE [PostId] = 2
  12. After SaveChanges:
  13. Blog '1' is in state Unchanged with 2 posts referenced.
  14. Post '1' is in state Detached with FK '1' and no reference to a blog.
  15. Post '2' is in state Detached with FK '1' and no reference to a blog.
  • 文章标记为已修改,因为断开关系导致外键标记为 null
    • 如果外键不可以为 null,则即使实际值标记为 null 也不会更改
  • SaveChanges 发送对依赖项/子项(文章)的删除
  • 保存后,依赖项/子项(文章)会分离,因为它们现在已从数据库中删除

具有必选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNullDeleteBehavior.ClientSetNull or DeleteBehavior.SetNull with required relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After making posts orphans:
  6. Blog '1' is in state Unchanged with 2 posts referenced.
  7. Post '1' is in state Modified with FK 'null' and no reference to a blog.
  8. Post '2' is in state Modified with FK 'null' and no reference to a blog.
  9. Saving changes:
  10. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 1
  11. SaveChanges threw DbUpdateException: Cannot insert the value NULL into column 'BlogId', table 'EFSaving.CascadeDelete.dbo.Posts'; column does not allow nulls. UPDATE fails. The statement has been terminated.
  • 文章标记为已修改,因为断开关系导致外键标记为 null
    • 如果外键不可以为 null,则即使实际值标记为 null 也不会更改
  • SaveChanges 尝试将文章外键设置为 null,但会失败,因为外键不可以为 null

具有可选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNullDeleteBehavior.ClientSetNull or DeleteBehavior.SetNull with optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After making posts orphans:
  6. Blog '1' is in state Unchanged with 2 posts referenced.
  7. Post '1' is in state Modified with FK 'null' and no reference to a blog.
  8. Post '2' is in state Modified with FK 'null' and no reference to a blog.
  9. Saving changes:
  10. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 1
  11. UPDATE [Posts] SET [BlogId] = NULL WHERE [PostId] = 2
  12. After SaveChanges:
  13. Blog '1' is in state Unchanged with 2 posts referenced.
  14. Post '1' is in state Unchanged with FK 'null' and no reference to a blog.
  15. Post '2' is in state Unchanged with FK 'null' and no reference to a blog.
  • 文章标记为已修改,因为断开关系导致外键标记为 null
    • 如果外键不可以为 null,则即使实际值标记为 null 也不会更改
  • SaveChanges 将依赖项/子项(文章)的外键设置为 null
  • 保存后,依赖项/子项(文章)现在具有 null 外键值,并且对删除的主体/父项(博客)的引用已删除

具有必选或可选关系的 DeleteBehavior.RestrictDeleteBehavior.Restrict with required or optional relationship

  1. After loading entities:
  2. Blog '1' is in state Unchanged with 2 posts referenced.
  3. Post '1' is in state Unchanged with FK '1' and reference to blog '1'.
  4. Post '2' is in state Unchanged with FK '1' and reference to blog '1'.
  5. After making posts orphans:
  6. Blog '1' is in state Unchanged with 2 posts referenced.
  7. Post '1' is in state Modified with FK '1' and no reference to a blog.
  8. Post '2' is in state Modified with FK '1' and no reference to a blog.
  9. Saving changes:
  10. SaveChanges threw InvalidOperationException: The association between entity types 'Blog' and 'Post' has been severed but the foreign key for this relationship cannot be set to null. If the dependent entity should be deleted, then setup the relationship to use cascade deletes.
  • 文章标记为已修改,因为断开关系导致外键标记为 null
    • 如果外键不可以为 null,则即使实际值标记为 null 也不会更改
  • 由于Restrict指告知 EF 不要自动将外键设置为 null,因此它保持为非 null,而 SaveChanges 将引发异常但不进行保存

级联到未跟踪的实体Cascading to untracked entities

调用“SaveChanges” 时,级联删除规则将应用于由上下文跟踪的所有实体。 这是上述所有示例的情况,即生成用于删除主体/父项(博客)和所有依赖项/子项(文章)的 SQL 的原因:

  1. DELETE FROM [Posts] WHERE [PostId] = 1
  2. DELETE FROM [Posts] WHERE [PostId] = 2
  3. DELETE FROM [Blogs] WHERE [BlogId] = 1

如果仅加载主体(例如,当为不含 Include(b => b.Posts) 的博客创建查询以包含文章时),则 SaveChanges 只会生成用于删除主体/父项的 SQL:

  1. DELETE FROM [Blogs] WHERE [BlogId] = 1

如果数据库已配置相应的级联行为,则只会删除依赖项/子项(文章)。 如果使用 EF 创建数据库,则会为你设置此级联行为。