級聯刪除

一个人走在路上發表於2024-03-30

級聯刪除
專案
2023/08/24
17 個參與者

反饋
本文內容
發生級聯行為時
發生級聯行為的位置
級聯 NULL
配置級聯行為
Entity Framework Core (EF Core) 表示使用外來鍵的關係。 具有外來鍵的實體是關係中的子實體或依賴實體。 此實體的外來鍵值必須與相關主體/父實體的主鍵值(或替換鍵值)匹配。

如果刪除主體/父實體,則依賴項/子項的外來鍵值將不再匹配任何主體/父實體的主鍵或替換鍵。 這是無效狀態,將導致在大多數資料庫中出現引用約束衝突。

可透過兩種方法來避免此引用約束衝突:

將外來鍵值設定為 null
同時刪除依賴實體/子實體
第一個選項僅適用於其中外來鍵屬性(及其對映到的資料庫列)必須可為 null 的可選關係。

第二個選項適用於任何型別的關係,它被稱作“級聯刪除”。

提示

本文件從更新資料庫的角度介紹級聯刪除(和刪除孤立項)。 本文大量使用在在 EF Core 中更改跟蹤和更改外來鍵和導航文章中介紹的概念。 請確保在此處處理材料之前充分了解這些概念。

提示

透過從 GitHub 下載示例程式碼,你可執行並除錯到本文件中的所有程式碼。

發生級聯行為時
當依賴實體/子實體無法再與其當前主體/父實體關聯時,需要執行級聯刪除。 發生這種情況的原因可能是主體/父實體已被刪除,或者當主體/父實體仍存在,但依賴實體/子實體不再與其關聯時。

刪除主體/父實體
請考慮此簡單模型,其中 Blog 是與 Post(依賴實體/子實體)的關係中的主體/父實體。 Post.BlogId 是一個外來鍵屬性,其值必須與該文章所屬部落格中的 Blog.Id 主鍵匹配。

C#

複製
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 的,因此該關係被配置為必需的。 預設情況下,所需的關係配置為使用級聯刪除。 要詳細瞭解建模關係,請參閱關係。

刪除部落格時,所有文章都將被級聯刪除。 例如:

C#

複製
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:

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:

C#

複製
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 集合導航中刪除每篇文章內容來斷開關係:

C#

複製
using var context = new BlogsContext();

var blog = context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).First();

blog.Posts.Clear();

context.SaveChanges();
無論哪種情況,結果都一樣:沒有刪除部落格,但是刪除了不再與任何部落格關聯的文章:

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;
刪除不再與任何主體/依賴實體關聯的實體這一行為被稱作“刪除孤立項”。

提示

級聯刪除和刪除孤立項是密切相關的。 當斷開與所需的主體/父實體之間的關係時,兩者都將導致刪除依賴實體/子實體。 對於級聯刪除,由於主體/父實體本身已刪除,因此發生了這種斷開。 對於孤立項,主體/父實體仍然存在,但不再與依賴實體/子實體相關。

發生級聯行為的位置
可將級聯行為應用於:

當前 DbContext 跟蹤的實體
資料庫中尚未載入到上下文中的實體
級聯刪除被跟蹤實體
EF Core 始終將配置的級聯行為應用於跟蹤的實體。 這意味著如上面的示例所示,如果應用程式將所有相關的依賴實體/子實體載入到 DbContext 中,則無論如何配置資料庫,都將正確應用級聯行為。

提示

可使用 ChangeTracker.CascadeDeleteTiming 和 ChangeTracker.DeleteOrphansTiming 控制在被跟蹤實體上發生級聯行為的確切時間。 有關詳細資訊,請參閱更改外來鍵和導航。

資料庫中的級聯刪除
許多資料庫系統還提供在資料庫中刪除實體時觸發的級聯行為。 使用 EnsureCreated 或 EF Core 遷移建立資料庫時,EF Core 會根據 EF Core 模型中的級聯刪除行為來配置這些行為。 例如,透過上述模型,使用 SQL Server 時將為文章建立下表:

SQL

複製
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 配置的。

如果我們知道資料庫是這樣配置的,那麼我們可以刪除部落格,而無需先載入文章,資料庫將負責刪除與此部落格相關的所有文章。 例如:

C#

複製
using var context = new BlogsContext();

var blog = context.Blogs.OrderBy(e => e.Name).First();

context.Remove(blog);

context.SaveChanges();
請注意,文章沒有 Include,因此它們不會被載入。 在這種情況下,SaveChanges 將僅刪除部落格,因為這是唯一正在跟蹤的實體:

SQL

複製
-- 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)對形成周期的級聯行為有限制。 例如,請考慮以下模型:

C#

複製
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; }
}
該模型具有 3 個關係,所有這些關係都是必需的,因此按約定配置為級聯刪除:

刪除部落格將級聯刪除所有相關文章
刪除文章的作者將導致作者的文章被級聯刪除
刪除部落格所有者將導致該部落格被級聯刪除
這一切都是合理的(不過在部落格管理策略中有些苛刻!),但是嘗試建立配置了這些級聯的 SQL Server 資料庫會導致以下異常:

Microsoft.Data.SqlClient.SqlException (0x80131904):將 FOREIGN KEY 約束“FK_Posts_Person_AuthorId”引入表“Posts”可能會導致迴圈或多重級聯路徑。 請指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 約束。

有兩種方法可處理這種情況:

將一個或多個關係更改為不級聯刪除。
配置資料庫,但不包含這些級聯刪除中的一個或多個,然後確保已載入所有依賴實體,以便 EF Core 可執行級聯行為。
在我們的示例中採用第一種方法,我們可透過為文章與部落格之間的關係賦予可為 null 的外來鍵屬性來使其成為可選關係:

C#

複製
public int? BlogId { get; set; }
可選關係使得即使沒有部落格,文章也可存在,這意味著預設情況下將不再配置級聯刪除。 這表示級聯操作不再迴圈,並且可以在 SQL Server 上建立資料庫而不會出現錯誤。

採取第二種方法,我們可以保持必需的部落格所有者關係並對其配置來進行級聯刪除,但是使此配置僅適用於跟蹤的實體,而不適用於資料庫:

C#

複製
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
現在,如果我們同時載入某使用者及其擁有的部落格,然後刪除該使用者,會發生什麼呢?

C#

複製
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 將級聯刪除所有者,以便部落格也被刪除:

SQL

複製
-- 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;
但是,如果在刪除所有者時未載入部落格:

C#

複製
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_People_OwnerId”衝突。 資料庫 "Scratch"、表 "dbo.Blogs"、列 "OwnerId" 中發生衝突。 語句已終止。

級聯 NULL
可選關係將可為 null 的外來鍵屬性對映到可為 null 的資料庫列。 這意味著當刪除當前主體/父實體或斷開與依賴實體/子實體的關係時,可將外來鍵值設定為 NULL。

讓我們再看一下發生級聯行為時的示例,但這次可選關係由可為 null 的 Post.BlogId 外來鍵屬性表示:

C#

複製
public int? BlogId { get; set; }
刪除每篇文章的相關部落格時,該文章的外來鍵屬性將設定為 NULL。 例如,此程式碼與之前的程式碼相同:

C#

複製
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

複製
-- 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;
同樣,如果使用上述任一示例來斷開關係:

C#

複製
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();
或:

C#

複製
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 外來鍵值更新文章:

SQL

複製
-- 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 年首版以來,實體框架預設情況下都會修復這類關係。 在 EF Core 之前,它沒有名稱,且無法更改。 它現在稱為 ClientSetNull,如下一部分所述。

當刪除可選關係中的主體/父實體時,資料庫也可配置為級聯 NULL。 但是,與在資料庫中使用級聯刪除相比,這種情況要少得多。 在使用 SQL Server 時,在資料庫中同時使用級聯刪除和級聯 NULL 幾乎總是會導致關係迴圈。 若要詳細瞭解如何配置級聯 NULL,請參閱下一部分。

配置級聯行為
提示

請務必閱讀上述部分,然後再轉到此處。 如果不瞭解上述資料,那麼配置選項可能沒有意義。

使用 OnModelCreating 中的 OnDelete 方法按關係配置級聯行為。 例如:

C#

複製
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 NO ACTION(資料庫預設值)和 ON DELETE RESTRICT 的行為通常相同或非常相似。 儘管 NO ACTION 可能意味著什麼,但這兩個選項都會導致強制執行引用約束。 區別是當有一個時,是影響資料庫何時檢查約束。 請檢視資料庫文件,瞭解資料庫系統上 ON DELETE NO ACTION 和 ON DELETE RESTRICT 之間的具體區別。

SQL Server 不支援 ON DELETE RESTRICT,因此改為使用 ON DELETE NO ACTION。

導致資料庫級聯行為的唯一值是 Cascade 和 SetNull。 所有其他值會將資料庫配置為不級聯任何更改。

對 SaveChanges 行為的影響
以下各部分中的表格介紹了刪除主體/父實體或斷開與主體/子實體的關係時,依賴實體/子實體所發生的情況。 每張表都涵蓋下述內容之一:

可選(可為 null 的外來鍵)和必需(不可為 null 的外來鍵)關係
依賴項/子項何時由 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 時,對必需關係使用除級聯刪除以外的其他方法將導致異常。
通常,這是來自 EF Core 的 InvalidOperationException,因為在已載入的子項/依賴項中檢測到無效狀態。
ClientNoAction 會強制 EF Core 在將依賴項傳送到資料庫之前不檢查修復它們,因此在這種情況下,資料庫將引發異常,然後由 SaveChanges 將其包裝在 DbUpdateException 中。
建立資料庫時會拒絕 SetNull,因為外來鍵列不可為 null。
由於已載入依賴項/子項,因此它們始終會被 EF Core 刪除,並且永遠不會留下來等到資料庫被刪除。
與未載入的依賴項/子項的必需關係

展開表
DeleteBehavior 刪除主體/父實體時 斷開與主體/父實體的關係時
Cascade 資料庫刪除的依賴項 不可用
限制 DbUpdateException 不可用
NoAction DbUpdateException 不可用
SetNull SqlException(建立資料庫時) 不可用
ClientSetNull DbUpdateException 不可用
ClientCascade DbUpdateException 不可用
ClientNoAction DbUpdateException 不可用
注意:

此處沒法斷開關係,因為未載入依賴項/子項。
這種必需關係的預設值為 Cascade。
呼叫 SaveChanges 時,對必需關係使用除級聯刪除以外的其他方法將導致異常。
通常,這是 DbUpdateException,理由是未載入依賴項/子項,因此資料庫只能檢測到無效狀態。 然後,SaveChanges 會將資料庫異常包裝在 DbUpdateException 中。
建立資料庫時會拒絕 SetNull,因為外來鍵列不可為 null。
與已載入的依賴項/子項的可選關係

展開表
DeleteBehavior 刪除主體/父實體時 斷開與主體/父實體的關係時
Cascade EF Core 刪除的依賴項 EF Core 刪除的依賴項
限制 EF Core 將依賴外來鍵設定為 NULL EF Core 將依賴外來鍵設定為 NULL
NoAction EF Core 將依賴外來鍵設定為 NULL EF Core 將依賴外來鍵設定為 NULL
SetNull EF Core 將依賴外來鍵設定為 NULL EF Core 將依賴外來鍵設定為 NULL
ClientSetNull EF Core 將依賴外來鍵設定為 NULL EF Core 將依賴外來鍵設定為 NULL
ClientCascade EF Core 刪除的依賴項 EF Core 刪除的依賴項
ClientNoAction DbUpdateException EF Core 將依賴外來鍵設定為 NULL
注意:

這種可選關係的預設值為 ClientSetNull。
永遠不會刪除依賴項/子項,除非配置了 Cascade 或 ClientCascade。
所有其他值都會導致 EF Core 將依賴外來鍵設定為 NULL...
... ClientNoAction 除外,它指示 EF Core 在刪除主體/父實體時不處理依賴項/子項的外來鍵。 因此,資料庫會引發異常,由 SaveChanges 將其包裝為 DbUpdateException。
與未載入的依賴項/子項的可選關係

展開表
DeleteBehavior 刪除主體/父實體時 斷開與主體/父實體的關係時
Cascade 資料庫刪除的依賴項 不可用
限制 DbUpdateException 不可用
NoAction DbUpdateException 不可用
SetNull 資料庫將依賴外來鍵設定為 NULL 不可用
ClientSetNull DbUpdateException 不可用
ClientCascade DbUpdateException 不可用
ClientNoAction DbUpdateException 不可用
注意:

此處沒法斷開關係,因為未載入依賴項/子項。
這種可選關係的預設值為 ClientSetNull。
除非已將資料庫配置為級聯刪除或級聯 NULL,否則必須載入依賴項/子項以避免資料庫異常。

相關文章