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 迁移或EnsureCreated创建的外键约束中每个OnDelete
值的结果。
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 关于创建数据库 |
无 |
客户设置为空 | DbUpdateException |
无 |
ClientCascade | DbUpdateException |
无 |
ClientNoAction | DbUpdateException |
无 |
注释:
- 在此解除关系无效,因为依赖关系/子项未加载。
- 此类所需关系的默认值为
Cascade
。 - 若在必须的关系中不使用级联删除,而使用其他方式,当调用 SaveChanges 时将会导致异常。
- 通常,这是一个
DbUpdateException
,因为未加载依赖项/子项,因此只有数据库才能检测到无效状态。 SaveChanges 随后会将数据库异常包装在一个DbUpdateException
中。 -
SetNull
创建数据库时被拒绝,因为外键列不可为 null。
- 通常,这是一个
与加载的依赖项/子级的可选关系
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 删除的依赖项 |
客户端无动作 | DbUpdateException |
EF Core 将依赖 FK 设置为 null |
注释:
- 此类可选关系的默认值为
ClientSetNull
。 - 除非已配置
Cascade
或ClientCascade
,否则永远不会删除从属/子级。 - 所有其他值都会导致 EF Core 将依赖 FK 设置为 null...
- ...除了
ClientNoAction
这个特例,该操作指示 EF Core 在删除主体或父级时不修改依赖项或子项的外键。 因此,数据库会引发异常,该异常被 SaveChanges 包装为DbUpdateException
。
- ...除了
与未加载依赖项/子项的可选关系
DeleteBehavior | 删除主体/父级时 | 从主体/父级分离时 |
---|---|---|
级联 | 数据库删除的依赖项 | 无 |
限制 | DbUpdateException |
无 |
无操作(NoAction) | DbUpdateException |
无 |
置空 | 按数据库设置为 null 的从属 FK | 无 |
客户端设置为空 | DbUpdateException |
无 |
ClientCascade | DbUpdateException |
无 |
ClientNoAction | DbUpdateException |
无 |
注释:
- 解除关系在此不可行,因为受扶养者/子女尚未加载。
- 此类可选关系的默认值为
ClientSetNull
。 - 必须加载依赖项/子项以避免数据库异常,除非数据库已配置为级联删除或级联空值。