關聯刪除通常是一個資料庫術語,用於描述在刪除行時允許自動觸發刪除關聯行的特征;即當主表的數據行被刪除時,自動將關聯表中依賴的數據行進行刪除,或者將外鍵更新為 或預設值。 資料庫關聯刪除行為 我們先來看一看SQL Server中支持的行為。在創建外鍵約束時,可以指定關聯表在主表刪除行時,對依賴的數據如 ...
關聯刪除通常是一個資料庫術語,用於描述在刪除行時允許自動觸發刪除關聯行的特征;即當主表的數據行被刪除時,自動將關聯表中依賴的數據行進行刪除,或者將外鍵更新為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
; - 因為要修改關聯實體的狀態或外鍵的值,所以關聯實體必須被跟蹤。