串聯刪除
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 = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
context.Remove(blog);
context.SaveChanges();
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 = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
context.SaveChanges();
從集合導覽中移除每個文章 Blog.Posts
,也可以切斷關聯性:
using var context = new BlogsContext();
var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
blog.Posts.Clear();
context.SaveChanges();
在任一情況下,結果都相同:不會刪除部落格,但不再與任何部落格相關聯的文章會遭到刪除:
-- 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.DeleteOrphansTiming來控制ChangeTracker.CascadeDeleteTiming追蹤實體發生串連行為的確切時機。 如需詳細資訊,請參閱變更外鍵和導覽。
資料庫中的串聯刪除
許多資料庫系統也提供在資料庫中刪除實體時所觸發的串聯行為。 EF Core 會根據使用 EnsureCreated 或 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 = context.Blogs.OrderBy(e => e.Name).First();
context.Remove(blog);
context.SaveChanges();
請注意, 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):在數據表 '貼文' 上引進 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 = context.People.Single(e => e.Name == "ajcvickers");
var blog = context.Blogs.Single(e => e.Owner == owner);
context.Remove(owner);
context.SaveChanges();
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 = context.People.Single(e => e.Name == "ajcvickers");
context.Remove(owner);
context.SaveChanges();
然後會因為資料庫中外鍵條件約束的違規而擲回例外狀況:
Microsoft.Data.SqlClient.SqlException:DELETE 語句與 REFERENCE 條件約束 “FK_Blogs_人員_OwnerId” 衝突。 資料庫 「Scratch」,數據表 「dbo」 中發生衝突。Blogs“, column 'OwnerId'。 陳述式已經結束。
串連 Null
選擇性關聯性具有對應至可為 Null 資料庫數據行的可為 Null 外鍵屬性。 這表示當目前的主體/父系遭到刪除或從相依/子系中斷時,外鍵值可以設定為 null。
讓我們再看看何時發生串連行為中的範例,但這次有選擇性的關聯性,以可為 Null 的Post.BlogId
外鍵屬性表示:
public int? BlogId { get; set; }
刪除相關部落格時,每個文章的這個外鍵屬性將會設定為 null。 例如,此程式代碼與之前相同:
using var context = new BlogsContext();
var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
context.Remove(blog);
context.SaveChanges();
呼叫 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 = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
foreach (var post in blog.Posts)
{
post.Blog = null;
}
context.SaveChanges();
或:
using var context = new BlogsContext();
var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();
blog.Posts.Clear();
context.SaveChanges();
然後,呼叫 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。 不過,這比在資料庫中使用串聯刪除要少得多。 同時使用資料庫中的串聯刪除和串連 Null,幾乎一律會在使用 SQL Server 時產生關聯性迴圈。 如需設定串連 Null 的詳細資訊,請參閱下一節。
設定串聯行為
提示
請務必先閱讀上述章節,再來這裡。 如果無法瞭解上述材料,組態選項可能會沒有意義。
級聯行為是使用 中的OnModelCreating方法,根據OnDelete關聯性設定。 例如:
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 | 對資料庫架構的影響 |
---|---|
Cascade | ON DELETE CASCADE |
限制 | ON DELETE RESTRICT |
NoAction | 資料庫預設值 |
SetNull | ON DELETE SET NULL |
ClientSetNull | 資料庫預設值 |
ClientCascade | 資料庫預設值 |
ClientNoAction | 資料庫預設值 |
(資料庫預設值)和ON DELETE RESTRICT
關係資料庫中的行為ON DELETE NO ACTION
通常相同或非常類似。 NO ACTION
儘管可能暗示這兩個選項都會導致強制執行引用條件約束。 當有一個時,差異在於 資料庫檢查條件約束時 。 檢查資料庫檔,以取得資料庫系統上和 ON DELETE RESTRICT
之間的特定差異ON DELETE NO ACTION
。
SQL Server 不支援 ON DELETE RESTRICT
,因此 ON DELETE NO ACTION
會改用 。
唯一會導致資料庫串聯行為的值是 Cascade
和 SetNull
。 所有其他值都會將資料庫設定為不會重疊任何變更。
對 SaveChanges 行為的影響
下列各節中的數據表涵蓋刪除主體/父系時相依/子實體,或其與相依/子實體的關聯性遭到切斷時會發生什麼事。 每個資料表涵蓋下列其中一個:
- 選擇性 (可為 Null 的 FK) 和必要 (不可為 Null 的 FK) 關聯性
- 當 DbContext 載入和追蹤相依/子系時,以及它們只存在於資料庫中時
載入相依專案/子系的必要關聯性
DeleteBehavior | 刪除主體/父系時 | 從主體/父系進行分割 |
---|---|---|
Cascade | EF Core 刪除的相依專案 | EF Core 刪除的相依專案 |
限制 | InvalidOperationException |
InvalidOperationException |
NoAction | InvalidOperationException |
InvalidOperationException |
SetNull | SqlException 在建立資料庫時 |
SqlException 在建立資料庫時 |
ClientSetNull | InvalidOperationException |
InvalidOperationException |
ClientCascade | EF Core 刪除的相依專案 | EF Core 刪除的相依專案 |
ClientNoAction | DbUpdateException |
InvalidOperationException |
注意:
- 這類必要關聯性的預設值為
Cascade
。 - 呼叫 SaveChanges 時,針對必要的關聯性使用串聯刪除以外的任何專案,將會導致例外狀況。
- 一般而言,這是
InvalidOperationException
來自 EF Core 的 ,因為載入的子系/相依項中偵測到無效的狀態。 ClientNoAction
會強制 EF Core 在將相依專案傳送至資料庫之前,不要檢查修正相依專案,因此在此情況下,資料庫會擲回例外狀況,然後由 SaveChanges 包裝在 中DbUpdateException
。SetNull
建立資料庫時會遭到拒絕,因為外鍵數據行不可為 Null。
- 一般而言,這是
- 由於相依專案/子系會載入,因此 EF Core 一律會刪除它們,而且永遠不會讓資料庫刪除。
未載入相依專案/子系的必要關聯性
DeleteBehavior | 刪除主體/父系時 | 從主體/父系進行分割 |
---|---|---|
Cascade | 資料庫刪除的相依專案 | N/A |
限制 | DbUpdateException |
N/A |
NoAction | DbUpdateException |
N/A |
SetNull | SqlException 在建立資料庫時 |
N/A |
ClientSetNull | DbUpdateException |
N/A |
ClientCascade | DbUpdateException |
N/A |
ClientNoAction | DbUpdateException |
N/A |
注意:
- 因為不會載入相依專案/子系,因此斷斷關聯性在這裡無效。
- 這類必要關聯性的預設值為
Cascade
。 - 呼叫 SaveChanges 時,針對必要的關聯性使用串聯刪除以外的任何專案,將會導致例外狀況。
- 一般而言,這是因為
DbUpdateException
不會載入相依專案/子系,因此資料庫只能偵測到無效的狀態。 SaveChanges 接著會將資料庫例外狀況包裝在 中DbUpdateException
。 SetNull
建立資料庫時會遭到拒絕,因為外鍵數據行不可為 Null。
- 一般而言,這是因為
載入相依專案/子系的選擇性關聯性
DeleteBehavior | 刪除主體/父系時 | 從主體/父系進行分割 |
---|---|---|
Cascade | EF Core 刪除的相依專案 | EF Core 刪除的相依專案 |
限制 | EF Core 將相依 FK 設定為 Null | EF Core 將相依 FK 設定為 Null |
NoAction | EF Core 將相依 FK 設定為 Null | EF Core 將相依 FK 設定為 Null |
SetNull | EF Core 將相依 FK 設定為 Null | EF Core 將相依 FK 設定為 Null |
ClientSetNull | EF Core 將相依 FK 設定為 Null | EF Core 將相依 FK 設定為 Null |
ClientCascade | EF Core 刪除的相依專案 | EF Core 刪除的相依專案 |
ClientNoAction | DbUpdateException |
EF Core 將相依 FK 設定為 Null |
注意:
- 這類選擇性關聯性的預設值為
ClientSetNull
。 - 除非或
ClientCascade
已設定,否則Cascade
永遠不會刪除相依專案/子系。 - 所有其他值都會讓 EF Core 將相依 FK 設定為 Null...
- ...例外
ClientNoAction
狀況會告知 EF Core 在刪除主體/父系時,不要觸碰相依專案/子系的外鍵。 因此,資料庫會擲回例外狀況,此例外狀況會包裝為DbUpdateException
SaveChanges。
- ...例外
未載入相依專案/子系的選擇性關聯性
DeleteBehavior | 刪除主體/父系時 | 從主體/父系進行分割 |
---|---|---|
Cascade | 資料庫刪除的相依專案 | N/A |
限制 | DbUpdateException |
N/A |
NoAction | DbUpdateException |
N/A |
SetNull | 依資料庫將相依 FK 設定為 null | N/A |
ClientSetNull | DbUpdateException |
N/A |
ClientCascade | DbUpdateException |
N/A |
ClientNoAction | DbUpdateException |
N/A |
注意:
- 因為不會載入相依專案/子系,因此斷斷關聯性在這裡無效。
- 這類選擇性關聯性的預設值為
ClientSetNull
。 - 除非資料庫已設定為重疊刪除或 Null,否則必須載入相依/子系以避免資料庫例外狀況。