Entity Framework Core 关联删除
关联删除通常是一个数据库术语,用于描述在删除行时允许自动触发删除关联行的特征;即当主表的数据行被删除时,自动将关联表中依赖的数据行进行删除,或者将外键更新为NULL或默认值。
数据库关联删除行为
我们先来看一看SQL Server中支持的行为。在创建外键约束时,可以指定关联表在主表删除行时,对依赖的数据如何执行操作。例如下面的SQL语句,[Order Details]表中[OrderID]字段 是外键,依赖于[Orders]表中的主键[OrderID]。
CREATE TABLE [Orders] (
    [OrderID] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    [OrderDate] datetime2 NULL,
    CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
);
GO
CREATE TABLE [Order Details] (
    [DetailId] int NOT NULL IDENTITY,
    [OrderID] int NULL,
    [ProductID] int NOT NULL,
    CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
    CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE SET NULL
);
外键约束[FK_Order Details_Orders_OrderID]末尾的语句是ON DELETE SET NULL,表示当主表的数据行删除时,自动将关联表数据行的外键更新为NULL。
在SQL Server中支持如下四种行为:
- ON DELETE NO ACTION
默认行为,删除主表数据行时,依赖表中的数据不会执行任何操作,此时会产生错误,并回滚DELETE语句。例如会产生下面的错误:DELETE 语句与 REFERENCE 约束"FK_Order Details_Orders_OrderID"冲突。该冲突发生于数据库"Northwind_Test",表"dbo.Order Details", column 'OrderID'。
语句已终止。 - ON DELETE CASCADE
删除主表数据行时,依赖表的中数据行也会同步删除。 - ON DELETE SET NULL
删除主表数据行时,将依赖表中数据行的外键更新为NULL。为了满足此约束,目标表的外键列必须可为空值。 - ON DELETE SET DEFAULT
删除主表数据行时,将依赖表的中数据行的外键更新为默认值。为了满足此约束,目标表的所有外键列必须具有默认值定义;如果外键可为空值,并且未显式设置默认值,则将使用NULL作为该列的隐式默认值。 
简单介绍了数据库中行为后,我们来着重介绍 EF Core 中的关联实体的行为。
定义实体
我们先定义两个实体Order、OrderDetail分别表示订单和订单明细;其中Order与OrderDetail的关系是一对多,在OrderDetail实体中OrderID表示外键,依赖于Order实体中的主键OrderID。
    public class Order
    {
        public int OrderID { get; set; }
        public string Name { get; set; }
        public DateTime? OrderDate { get; set; }
        public ICollection<OrderDetail> OrderDetails { get; set; }
    }
    public class OrderDetail
    {
        public int DetailId { get; set; }
        public int? OrderID { get; set; }
        public int ProductID { get; set; }
        public Order Order { get; set; }
    }
Fluent API 配置关联实体
在DbContext中OnModelCreating方法中,我们使用 Fluent API 配置实体中之间的关系。
    public class NorthwindContext : DbContext
    {
        public virtual DbSet<Order> Orders { get; set; }
        public virtual DbSet<OrderDetail> OrderDetails { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Order>(
                builder =>
                {
                    builder.HasMany<OrderDetail>(e => e.OrderDetails).WithOne(e => e.Order).HasForeignKey(e => e.OrderID).OnDelete(DeleteBehavior.ClientSetNull);
                });
        }
    }
在OnDelete方法中,需要传递参数DeleteBehavior枚举,分别有如下四个值:
    public enum DeleteBehavior
    {
        Cascade,
        SetNull,
        ClientSetNull,
        Restrict
    }
这四个枚举值的分别表示不同的行为,这也是我们今天的重点。
创建表结构
我们分别使用使用这这个枚举值,来创建数据表结构。
        [InlineData(DeleteBehavior.Cascade)]
        [InlineData(DeleteBehavior.SetNull)]
        [InlineData(DeleteBehavior.ClientSetNull)]
        [InlineData(DeleteBehavior.Restrict)]
        [Theory]
        public void Create_Database(DeleteBehavior behavior)
        {
            using (var northwindContext = new NorthwindContext(behavior))
            {
                northwindContext.Database.EnsureDeleted();
                northwindContext.Database.EnsureCreated();
            }
        }
四个枚举值创建表的SQL语句类似如下,唯一区别在于创建外键约束[FK_Order Details_Orders_OrderID]中ON DELETE {} 后面的语句。
CREATE TABLE [Orders] (
    [OrderID] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    [OrderDate] datetime2 NULL,
    CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
);
GO
CREATE TABLE [Order Details] (
    [DetailId] int NOT NULL IDENTITY,
    [OrderID] int NOT NULL,
    [ProductID] int NOT NULL,
    CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
    CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE CASCADE
);
四个枚举值分别对应的SQL语句如下:
| 枚举值 | SQL语句 | 
|---|---|
| DeleteBehavior.Cascade | ON DELETE CASCADE | 
| DeleteBehavior.SetNull | ON DELETE SET NULL | 
| DeleteBehavior.ClientSetNull | ON DELETE NO ACTION | 
| DeleteBehavior.Restrict | ON DELETE NO ACTION | 
EF Core 关联实体删除行为
我们分别通过枚举值与是否跟踪关联实体,进行代码测试,测试代码如下:
        [InlineData(DeleteBehavior.Cascade, true)]
        [InlineData(DeleteBehavior.Cascade, false)]
        [InlineData(DeleteBehavior.SetNull, true)]
        [InlineData(DeleteBehavior.SetNull, false)]
        [InlineData(DeleteBehavior.ClientSetNull, true)]
        [InlineData(DeleteBehavior.ClientSetNull, false)]
        [InlineData(DeleteBehavior.Restrict, true)]
        [InlineData(DeleteBehavior.Restrict, false)]
        [Theory]
        public void Execute(DeleteBehavior behavior, bool includeDetail)
        {
            using (var northwindContext = new NorthwindContext(behavior))
            {
                northwindContext.Database.EnsureDeleted();
                northwindContext.Database.EnsureCreated();
            }
            int orderId;
            int detailId;
            using (var northwindContext = new NorthwindContext(behavior))
            {
                var order = new Order {
                    Name = "Order1"
                };
                var orderDetail = new OrderDetail {
                    ProductID = 11
                };
                order.OrderDetails = new List<OrderDetail> {
                    orderDetail
                };
                northwindContext.Set<Order>().Add(order);
                                northwindContext.SaveChanges();
                orderId = order.OrderID;
                detailId = orderDetail.DetailId;
            }
            using (var northwindContext = new NorthwindContext(behavior))
            {
                var queryable = northwindContext.Set<Order>().Where(e => e.OrderID == orderId);
                if (includeDetail){
                    queryable = queryable.Include(e => e.OrderDetails);
                }
                var order = queryable.Single();
                northwindContext.Set<Order>().Remove(order);
                try
                {
                    northwindContext.SaveChanges();
                    DumpSql();
                }
                catch (Exception)
                {
                    DumpSql();
                    throw;
                }
            }
            using (var northwindContext = new NorthwindContext(behavior))
            {
                var orderDetail = northwindContext.Set<OrderDetail>().Find(detailId);
                if (behavior == DeleteBehavior.Cascade)
                {
                    Assert.Null(orderDetail);
                }
                else
                {
                    Assert.NotNull(orderDetail);
                }
            }
        }
| 枚举值 | 是否跟踪关联实体 | 是否成功调用SaveChange | 关联实体是否存在 | 执行的SQL | 
|---|---|---|---|---|
| DeleteBehavior.Cascade | No | 成功 | 否 | DELETE FROM [Orders] WHERE [OrderID] = 1 | 
| DeleteBehavior.Cascade | YES | 成功 | 否 | DELETE FROM [Order Details] WHERE [DetailId] = 1 DELETE FROM [Orders] WHERE [OrderID] = 1  | 
| DeleteBehavior.SetNull | No | 成功 | YES (外键为 NULL) | 
DELETE FROM [Orders] WHERE [OrderID] = 1 | 
| DeleteBehavior.SetNull | YES | 成功 | YES (外键为 NULL) | 
UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1 DELETE FROM [Orders] WHERE [OrderID] = 1  | 
| DeleteBehavior.ClientSetNull | No | 失败 (外键约束)  | 
YES | DELETE FROM [Orders] WHERE [OrderID] = 1 | 
| DeleteBehavior.ClientSetNull | YES | 成功 | YES (外键为 NULL) | 
UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1 DELETE FROM [Orders] WHERE [OrderID] = 1  | 
| DeleteBehavior.Restrict | No | 失败 (外键约束)  | 
YES | DELETE FROM [Orders] WHERE [OrderID] = 1 | 
| DeleteBehavior.Restrict | YES | 失败 (外键约束)  | 
YES | DELETE FROM [Orders] WHERE [OrderID] = 1 | 
总结
根据上面的测试结果,我们可以出得如下结论:
DeleteBehavior.Cascade
- 如果关联实体未被跟踪,主实体的状态标记为删除,执行
SaveChage时,在删除主表的数据的同时,通过数据库的行为删除关联表的数据行; - 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的状态也会标记为删除,执行
SaveChange时,先删除关联表的数据行,然后再删除主表的数据行; - 外键可以设置非空值、也可以设置为可为空值;
 - 关联实体可以不被跟踪。
 
DeleteBehavior.SetNull
- 如果关联实体未被跟踪,主实体的状态标记为删除,执行
SaveChage时,在删除主表的数据时,通过数据库的行为将关联表数据行的外键更新为NULL,; - 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的外键会被设置为
null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行 ,然后删除主表的数据行; - 因为要将外键更新为
NULL,所以外键必须设置为可空字段; - 关联实体可以不被跟踪。
 
DeleteBehavior.ClientSetNull
- 数据库不会执行任何行为;
 - 关联实体必须被跟踪,将主实体的状态标记为删除时,关联实体的外键被设置为
null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行,然后删除主表的数据行(此时的行为与DeleteBehavior.SetNull一致); - 因为要将外键更新为
NULL,所以外键必须设置为可空字段; - 关联实体必须被跟踪,否则保存数据时会抛出异常。
 
DeleteBehavior.Restrict
- 框架不执行任何操作,由开发人员决定关联实体的行为,可以将关联实体的状态设置为删除,也可以将关联实体的外键设置为
null; - 因为要修改关联实体的状态或外键的值,所以关联实体必须被跟踪。
 
Entity Framework Core 关联删除的更多相关文章
- Entity Framework Core 软删除与查询过滤器
		
本文翻译自<Entity Framework Core: Soft Delete using Query Filters>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 注意 ...
 - 【EF】Entity Framework Core 软删除与查询过滤器
		
本文翻译自<Entity Framework Core: Soft Delete using Query Filters>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 注意 ...
 - Entity Framework Core系列之DbContext(删除)
		
上一篇我们介绍了Entity Framework Core系列之DbContext(修改),这一篇我们介绍下删除数据 修改实体的方法取决于context是否正在跟踪需要删除的实体. 下面的示例中con ...
 - 全自动迁移数据库的实现 (Fluent NHibernate, Entity Framework Core)
		
在开发涉及到数据库的程序时,常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况,这时就需要进行数据库迁移. 实现数据库迁移有很多种办法,从手动管理各个版本的ddl脚本,到实现自己的mig ...
 - Working with Data » Getting started with ASP.NET Core and Entity Framework Core using Visual Studio » 创建复杂数据模型
		
Creating a complex data model 创建复杂数据模型 8 of 9 people found this helpful The Contoso University sampl ...
 - Working with Data » 使用Visual Studio开发ASP.NET Core MVC and Entity Framework Core初学者教程
		
原文地址:https://docs.asp.net/en/latest/data/ef-mvc/intro.html The Contoso University sample web applica ...
 - .Net Core 2.0生态(4):Entity Framework Core 2.0 特性介绍和使用指南
		
前言 这是.Net Core 2.0生态生态介绍的最后一篇,EF一直是我喜欢的一个ORM框架,随着版本升级EF也发展到EF6.x,Entity Framework Core是一个支持跨平台的全新版本, ...
 - Entity Framework Core 2.0 入门简介
		
不多说废话了, 直接切入正题. EF Core支持情况 EF Core的数据库Providers: 此外还即将支持CosmosDB和 Oracle. EFCore 2.0新的东西: 查询: EF.Fu ...
 - Entity Framework Core 2.0 入门
		
该文章比较基础, 不多说废话了, 直接切入正题. 该文分以下几点: 创建Model和数据库 使用Model与数据库交互 查询和保存关联数据 EF Core支持情况 EF Core的数据库Provide ...
 
随机推荐
- 《你必须掌握的Entity Framework 6.x与Core 2.0》书籍出版
			
前言 到目前为止写过刚好两百来篇博客,看过我博客的读者应该大概知道我每一篇博客都沿袭着一贯的套路,从前言到话题最终到总结,本文依然是一如既往的套路,但是不是介绍技术,也可说是介绍技术,不过是介绍书中的 ...
 - 百度AI技术QQ群
			
百度语音QQ群 648968704 视频分析QQ群 632473158 DuerOSQQ群 604592023 图像识别QQ群 649285136 文字识别QQ群 631977213 理解与交互技术U ...
 - Ocelot中文文档-负载均衡
			
Ocelot能通过可用的下游服务对每个ReRoute进行负载平衡. 这意味着您可以扩展您的下游服务,并且Ocelot可以有效地使用它们. 可用的负载均衡器的类型是: LeastConnection - ...
 - 下载Github上某个项目的子文件夹和单个文件
			
preface Github下的项目可能很大,里面有很多的子文件夹,我们可能只需要使用某个子目录下的资源,可以不用下载完整的repo就能使用. 例如,我想下载这个repo中的字典文件:https:// ...
 - Python 30分钟入门指南
			
Python 30分钟入门指南 为什么 OIer 要学 Python? Python 语言特性简洁明了,使用 Python 写测试数据生成器和对拍器,比编写 C++ 事半功倍. Python 学习成本 ...
 - AE、AS调用时用代码提供许可(不需要添加LicenseControl控件)
			
private void CheckBindLicense() { ESRI.ArcGIS.RuntimeManager.Bind(ESRI.ArcGIS.ProductCode.EngineOrDe ...
 - httpd基础配置和虚拟主机的配置方法
			
RedHat6.5 httpd实验的大概步骤 #解包 tar zxf httpd-2.2.17.tar.gz -C /usr/src#切换到目录 cd /usr/src/httpd-2.2.17/# ...
 - 读取本地outlook邮件内容
			
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
 - MyBatis缓存详解
			
MyBatis缓存分为一级缓存和二级缓存 http://www.cnblogs.com/zemliu/archive/2013/08/05/3239014.html mybatis 二级cache h ...
 - 一个很有趣的示例Spring Boot项目,使用Giraphe CMS和Spring Boot
			
6: 这是一个很有趣的示例Spring Boot项目,使用Giraphe CMS和Spring Boot. Giraphe是基于Spring Boot的CMS框架. https://github.co ...