关联删除通常是一个数据库术语, 用于描述在删除行时允许自动触发删除关联行的特征; 即当主表的数据行被删除时, 自动将关联表中依赖的数据行进行删除, 或者将外键更新为 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 方法中, 需要传递参数 https://github.com/aspnet/EntityFrameworkCore/blob/rel/2.0.0/src/EFCore/DeleteBehavior.cs 枚举, 分别有如下四个值:
- 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 | 失败 < br ztid="144" ow="0" oh="0">(外键约束) | 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 | 失败 < br ztid="159" ow="0" oh="0">(外键约束) | YES | DELETE FROM [Orders] WHERE [OrderID] = 1 |
DeleteBehavior.Restrict | YES | 失败 < br ztid="166" ow="0" oh="0">(外键约束) | 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;
因为要修改关联实体的状态或外键的值, 所以关联实体必须被跟踪.
来源: https://www.cnblogs.com/tdfblog/p/entity-framework-core-cascade-delete.html