Entity Framework Core (EF Core) 表示使用外键的关系。 具有外键的实体是关系中的子实体或从属实体。 此实体的外键值必须与相关主体/父实体的主键值(或备用键值)匹配。
如果删除主体/父实体,则依赖/子级的外键值将不再匹配 任何 主体/父项的主键或备用键。 这是一种无效状态,会导致大多数数据库中出现引用约束冲突。
有两个选项可以避免此引用约束冲突:
- 将 FK 值设置为 null
- 同时删除依赖实体/子实体
第一个选项仅适用于可选关系,其中外键属性(及其映射的数据库列)必须为 null。
第二个选项对任何类型的关系有效,称为“级联删除”。
小窍门
本文档从更新数据库的角度介绍了级联删除(和删除孤立项)。 它大量使用在 EF Core 中的更改跟踪 和 更改外键和导航中引入的概念。 在处理此处的材料之前,请务必充分了解这些概念。
小窍门
可以通过 从 GitHub 下载示例代码来运行和调试本文档中的所有代码。
发生级联行为时
当依赖/子实体无法再与其当前主体/父实体关联时,需要级联删除。 发生这种情况可能是因为主体/父级被删除,或者当主体/父级仍然存在但依赖/子元素不再与其关联时,可能会发生这种情况。
删除主体/父级
请考虑此简单模型,其中Blog为主体/父级,Post为依赖/子级。
Post.BlogId 是外键属性,其值必须与文章所属博客的主键匹配 Blog.Id 。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
按照约定,此关系配置为必需,因为 Post.BlogId 外键属性不可为 null。 默认情况下,必需关系被配置为使用级联删除。 有关建模关系的详细信息,请参阅 “关系 ”。
删除博客时,所有文章将被级联删除。 例如:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
SaveChanges 使用 SQL Server 生成以下 SQL,例如:
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
断断关系
我们可以改为断断每个文章与其博客之间的关系,而不是删除博客。 为此,可将每个文章的引用导航 Post.Blog 设置为 null:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
await context.SaveChangesAsync();
还可以通过从 Blog.Posts 集合导航中删除每个帖子来切断关系:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
blog.Posts.Clear();
await context.SaveChangesAsync();
在任一情况下,结果都是相同的:不会删除博客,但不再与任何博客关联的文章将被删除:
-- Executed DbCommand (1ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p0='2'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Posts]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
删除不再与任何主体/依赖实体关联的实体称为“删除孤立项”。
小窍门
级联删除和删除孤儿记录密切相关。 这两者都会导致在与所需主体/父实体的关系被切断时删除依赖实体/子实体。 对于级联删除,断开是因为主体/父级本身被删除。 对于孤立的孤儿实体,主要/父实体仍然存在,但不再与从属/子实体有联系。
发生级联行为的位置
级联行为可以应用于:
- 当前跟踪的实体 DbContext
- 尚未加载到上下文中的数据库实体
跟踪实体的级联删除
EF Core 始终将配置的级联行为应用于跟踪的实体。 这意味着,如果应用程序将所有相关依赖实体/子实体加载到 DbContext 中,如上面的示例所示,则无论数据库配置方式如何,都会正确应用级联行为。
小窍门
可以使用 ChangeTracker.CascadeDeleteTiming 和 ChangeTracker.DeleteOrphansTiming控制跟踪实体发生级联行为的确切时间。 有关详细信息 ,请参阅更改外键和导航 。
数据库中的级联删除
许多数据库系统还提供在数据库中删除实体时触发的级联行为。 当使用 EnsureCreated 或 EF Core 迁移创建数据库时,EF Core 基于 EF Core 模型中的级联删除行为配置这些行为。 例如,使用上面的模型,在使用 SQL Server 时,将为帖子创建下表:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
[BlogId] int NOT NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) ON DELETE CASCADE
);
请注意,将博客与文章之间关系的外键约束配置为 ON DELETE CASCADE。
如果我们知道数据库已按如下所示进行配置,则可以删除博客 ,而无需先加载文章 ,数据库将负责删除与该博客相关的所有文章。 例如:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
请注意,由于帖子没有 Include,因此不会加载。 在这种情况下,SaveChanges 将仅删除博客,因为这是唯一要跟踪的实体:
-- Executed DbCommand (6ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
如果数据库中外键约束没有配置为级联删除,则会导致异常。 但是,在这种情况下,数据库会删除帖子,因为它在创建时已配置 ON DELETE CASCADE 。
注释
数据库通常没有任何方法可以自动删除孤立记录。 这是因为,虽然 EF Core 是通过导航和外键来表示关系,但数据库没有导航而只有外键。 这意味着,通常情况下,若不将双方加载到 DbContext,就不可能解除关系。
注释
EF Core 内存中数据库当前不支持数据库中的级联删除。
警告
软删除实体时不要在数据库中配置级联删除。 这可能会导致实体被意外真正删除,而不是软删除。
数据库级联限制
某些数据库(尤其是 SQL Server)对形成周期的级联行为有限制。 例如,请考虑以下模型:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
public int OwnerId { get; set; }
public Person Owner { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public int AuthorId { get; set; }
public Person Author { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
public Blog OwnedBlog { get; set; }
}
此模型具有三种关系,全部必需,因此配置为按约定级联删除:
- 删除博客将同时删除所有相关帖子
- 删除帖子的作者将导致其创作的帖子被级联删除。
- 删除博客所有者将导致博客被级联删除
这一切都是合理的(虽然博客管理策略有些严厉!),但尝试配置这些级联来创建 SQL Server 数据库会引发以下异常:
Microsoft.Data.SqlClient.SqlException (0x80131904):在表“Post”上引入 FOREIGN KEY 约束“FK_Posts_Person_AuthorId”可能会导致循环或多个级联路径。 请指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。
有两种方法可以处理这种情况:
- 更改一个或多个关系以使其不进行级联删除。
- 在没有一个或多个此类级联删除的情况下配置数据库,然后确保加载所有依赖实体,以便 EF Core 可以执行级联行为。
通过示例采用第一种方法,我们可以通过为其提供可为 null 的外键属性来使博客后的关系可选:
public int? BlogId { get; set; }
可选关系允许帖子在没有博客的情况下存在,这意味着默认情况下将不再配置级联删除。 这意味着在级联作中不再存在循环,可以在 SQL Server 上创建数据库而不出错。
改用第二种方法,我们可以保留博客与所有者的关系,并配置为级联删除,但使此配置仅适用于被跟踪的实体,而不是整个数据库。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
现在,如果我们同时加载一个人和他们拥有的博客,那么会发生什么情况,然后删除该人员?
using var context = new BlogsContext();
var owner = await context.People.SingleAsync(e => e.Name == "ajcvickers");
var blog = await context.Blogs.SingleAsync(e => e.Owner == owner);
context.Remove(owner);
await context.SaveChangesAsync();
EF Core 将级联删除所有者,以便同时删除博客:
-- Executed DbCommand (8ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p0;
SELECT @@ROWCOUNT;
-- Executed DbCommand (2ms) [Parameters=[@p1='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [People]
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
但是,如果在删除所有者时博客未加载:
using var context = new BlogsContext();
var owner = await context.People.SingleAsync(e => e.Name == "ajcvickers");
context.Remove(owner);
await context.SaveChangesAsync();
然后,由于违反数据库中的外键约束,将引发异常:
Microsoft.Data.SqlClient.SqlException:DELETE 语句与 REFERENCE 约束“FK_Blogs_People_OwnerId”冲突。 数据库“Scratch”中表“dbo.Blogs”的列“OwnerId”发生冲突。 语句已终止。
级联空值
可选关系具有可为空的外键属性,这些属性映射到可为空的数据库列。 这意味着,当当前主体/父项被删除或从依赖/子级中切断时,可以将外键值设置为 null。
我们再次看看发生级联行为时的示例,但这次包含一个可选关系,由可以为null的Post.BlogId外键属性表示:
public int? BlogId { get; set; }
当相关博客被删除时,每个帖子的外键属性将被设置为 null。 例如,此代码与之前的代码相同:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
context.Remove(blog);
await context.SaveChangesAsync();
现在将在调用 SaveChanges 时生成以下数据库更新:
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (1ms) [Parameters=[@p2='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [Blogs]
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;
同样,如果使用上述任一示例来切断关系:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
await context.SaveChangesAsync();
或者:
using var context = new BlogsContext();
var blog = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();
blog.Posts.Clear();
await context.SaveChangesAsync();
然后,在调用 SaveChanges 时,记录的外键值将更新为 null。
-- Executed DbCommand (2ms) [Parameters=[@p1='1', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
-- Executed DbCommand (0ms) [Parameters=[@p1='2', @p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [BlogId] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;
有关 EF Core 在更改其值时如何管理外键和导航的详情,请参阅 外键和导航的变更。
注释
自 2008 年第一个版本以来,此类关系的修复一直是 Entity Framework 的默认行为。 在 EF Core 之前,它没有名称,并且无法更改。 它现在称为ClientSetNull,如下一部分中所述。
数据库还可以配置为在删除可选关系中的主体/父级时级联设置为 null。 但是,这比在数据库中使用级联删除要少得多。 在数据库中同时使用级联删除和级联置空几乎总是会导致在使用 SQL Server 时出现关系循环。 有关配置级联 null 的详细信息,请参阅下一部分。
配置级联行为
小窍门
在来这里之前,请务必阅读上述部分。 如果无法理解上述材料,则配置选项可能没有意义。
使用OnDelete方法按每个OnModelCreating关系配置级联行为。 例如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
有关配置实体类型之间的关系的详细信息,请参阅 “关系 ”。
OnDelete 接受一个来自DeleteBehavior枚举类型的值,虽然这个枚举让人感到困惑。 此枚举定义了 EF Core 在跟踪实体上的行为,以及当 EF 用于创建架构时在数据库中配置级联删除。
对数据库架构的影响
下表展示了 EF Core 迁移或OnDelete创建的外键约束中每个EnsureCreated值的结果。
| DeleteBehavior | 对数据库架构的影响 |
|---|---|
| 级联 | ON DELETE CASCADE(删除时级联) |
| 限制 | "ON DELETE RESTRICT"(删除限制) |
| 无动作 | 数据库默认值 |
| 设置为空 | 删除时置空 |
| ClientSetNull | 数据库默认值 |
| ClientCascade | 数据库默认值 |
| ClientNoAction | 数据库默认值 |
ON DELETE NO ACTION(数据库默认值)和ON DELETE RESTRICT在关系数据库中的行为通常相同或非常相似。 尽管NO ACTION可能暗示不同,但这两个选项都会导致引用约束的执行。 当存在差异时,是因为数据库何时检查约束。 查看您的数据库文档,了解数据库系统中ON DELETE NO ACTION与ON DELETE RESTRICT之间的具体差异。
SQL Server 不支持 ON DELETE RESTRICT,因此 ON DELETE NO ACTION 改用。
唯一会导致数据库级联行为的值是 Cascade 和 SetNull。 所有其他值将配置数据库为不进行任何级联更改。
对 SaveChanges 行为的影响
以下部分中的表介绍了删除主体/父实体时依赖/子实体会发生什么情况,或者其与依赖/子实体的关系被切断。 每个表涵盖以下各项之一:
- 可选 (可为 null 的 FK) 和必需 (不可为 null 的 FK) 关系
- 当 DbContext 加载和跟踪依赖项/子项时,以及当它们仅存在于数据库中时
与依赖者/儿童建立关系已加载
| DeleteBehavior | 删除主控/父节点时 | 从主体/上级分离时 |
|---|---|---|
| 级联 | EF Core 删除的依赖项 | EF Core 删除的依赖项 |
| 限制 | InvalidOperationException |
InvalidOperationException |
| 无动作 | InvalidOperationException |
InvalidOperationException |
| 设置为空 |
SqlException 关于创建数据库 |
SqlException 关于创建数据库 |
| ClientSetNull | InvalidOperationException |
InvalidOperationException |
| ClientCascade | EF Core 删除的依赖项 | EF Core 删除的依赖项 |
| ClientNoAction | DbUpdateException |
InvalidOperationException |
注释:
- 此类所需关系的默认值为
Cascade。 - 使用级联删除以外的任何方式处理必要关系,将导致在调用 SaveChanges 时出现异常。
- 通常,这是 EF Core 中的一个
InvalidOperationException,因为加载的子级/从属项中检测到无效状态。 -
ClientNoAction强制 EF Core 在将依赖项发送到数据库之前不检查修复其依赖项,因此在这种情况下,数据库会引发异常,然后 SaveChanges 将其包装在DbUpdateException中。 -
SetNull创建数据库时被禁止,因为外键列不可为空。
- 通常,这是 EF Core 中的一个
- 由于依赖项/子级已加载,EF Core 始终会删除它们,而不会留给数据库来删除。
未加载与依赖关系/子女需要的关联
| DeleteBehavior | 删除主控/父节点时 | 从主体/上级分离时 |
|---|---|---|
| 级联 | 数据库删除的依赖项 | 无 |
| 限制 | DbUpdateException |
无 |
| 无动作 | DbUpdateException |
无 |
| 设置为空 |
SqlException 关于创建数据库 |
无 |
| ClientSetNull | DbUpdateException |
无 |
| ClientCascade | DbUpdateException |
无 |
| ClientNoAction | DbUpdateException |
无 |
注释:
- 在此解除关系无效,因为依赖关系/子项未加载。
- 此类所需关系的默认值为
Cascade。 - 使用级联删除以外的任何方式处理必要关系,将导致在调用 SaveChanges 时出现异常。
- 通常,这是一个
DbUpdateException,因为未加载依赖项/子项,因此只有数据库才能检测到无效状态。 SaveChanges 随后会将数据库异常包装在一个DbUpdateException中。 -
SetNull创建数据库时被禁止,因为外键列不可为空。
- 通常,这是一个
与加载的依赖项/子级的可选关系
| DeleteBehavior | 删除主控/父节点时 | 从主体/上级分离时 |
|---|---|---|
| 级联 | EF Core 删除的依赖项 | EF Core 删除的依赖项 |
| 限制 | EF Core 设置为 null 的依赖 FK | EF Core 设置为 null 的依赖 FK |
| 无动作 | EF Core 设置为 null 的依赖 FK | EF Core 设置为 null 的依赖 FK |
| 设置为空 | EF Core 设置为 null 的依赖 FK | EF Core 设置为 null 的依赖 FK |
| ClientSetNull | EF Core 设置为 null 的依赖 FK | EF Core 设置为 null 的依赖 FK |
| ClientCascade | EF Core 删除的依赖项 | EF Core 删除的依赖项 |
| ClientNoAction | DbUpdateException |
EF Core 设置为 null 的依赖 FK |
注释:
- 此类可选关系的默认值为
ClientSetNull。 - 除非已配置
Cascade或ClientCascade,否则永远不会删除从属/子级。 - 所有其他值都会导致 EF Core 将依赖 FK 设置为 null...
- ...除了
ClientNoAction这个特例,该操作指示 EF Core 在删除主体或父级时不修改依赖项或子项的外键。 因此,数据库会引发异常,该异常被 SaveChanges 包装为DbUpdateException。
- ...除了
与未加载依赖项/子项的可选关系
| DeleteBehavior | 删除主控/父节点时 | 从主体/上级分离时 |
|---|---|---|
| 级联 | 数据库删除的依赖项 | 无 |
| 限制 | DbUpdateException |
无 |
| 无动作 | DbUpdateException |
无 |
| 设置为空 | 按数据库设置为 null 的从属 FK | 无 |
| ClientSetNull | DbUpdateException |
无 |
| ClientCascade | DbUpdateException |
无 |
| ClientNoAction | DbUpdateException |
无 |
注释:
- 在此解除关系无效,因为依赖关系/子项未加载。
- 此类可选关系的默认值为
ClientSetNull。 - 必须加载依赖项/子项以避免数据库异常,除非数据库已配置为级联删除或级联空值。