关联删除通常是一个数据库术语,用于描述在删除行时允许自动触发删除关联行的特征;即当主表的数据行被删除时,自动将关联表中依赖的数据行进行删除,或者将外键更新为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
; - 因为要修改关联实体的状态或外键的值,所以关联实体必须被跟踪。