共用方式為


級聯刪除

Entity Framework Core (EF Core) 代表使用外鍵的關聯性。 具有外鍵的實體是關聯性中的子實體或相依實體。 此實體的外鍵值必須符合相關主體/父實體的主鍵值(或替代索引鍵值)。

如果刪除主體/父實體,則相依/子系的外鍵值將不再符合 任何 主體/父系的主鍵或替代索引鍵。 這是無效的狀態,而且會在大部分資料庫中造成引用條件約束違規。

有兩個選項可避免此引用條件約束違規:

  1. 將 FK 值設定為 null
  2. 同時刪除相依實體和子實體

第一個選項僅適用於選擇性關聯性,其中外鍵屬性(及其對應的資料庫數據行)必須是可為 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 = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();

context.Remove(blog);

await context.SaveChangesAsync();

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 = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();

foreach (var post in blog.Posts)
{
    post.Blog = null;
}

await context.SaveChangesAsync();

從集合導覽中移除每個文章 Blog.Posts ,也可以切斷關聯性:

using var context = new BlogsContext();

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

blog.Posts.Clear();

await context.SaveChangesAsync();

在任一情況下,結果都相同:不會刪除部落格,但不再與任何部落格相關聯的文章會遭到刪除:

-- 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.CascadeDeleteTimingChangeTracker.DeleteOrphansTiming來控制追蹤實體發生串連行為的確切時機。 如需詳細資訊 ,請參閱變更外鍵和導覽

資料庫中的連鎖刪除

許多資料庫系統也提供在資料庫中刪除實體時所觸發的串聯行為。 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 = await context.Blogs.OrderBy(e => e.Name).FirstAsync();

context.Remove(blog);

await context.SaveChangesAsync();

請注意,沒有 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 條件約束。

有兩種方式可以處理這種情況:

  1. 將一或多個關聯性變更為不串聯刪除。
  2. 配置資料庫時不包含一或多個級聯刪除功能,然後確保已載入所有相依實體,以便 EF Core 能夠執行級聯行為。

採用我們範例中的第一種方法,我們可以通過提供一個可空的外鍵屬性,使文章與部落格之間的關聯成為可選的。

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 = await context.People.SingleAsync(e => e.Name == "ajcvickers");
var blog = await context.Blogs.SingleAsync(e => e.Owner == owner);

context.Remove(owner);

await context.SaveChangesAsync();

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 = await context.People.SingleAsync(e => e.Name == "ajcvickers");

context.Remove(owner);

await context.SaveChangesAsync();

然後會因為資料庫中外鍵條件約束的違規而擲回例外狀況:

Microsoft.Data.SqlClient.SqlException:DELETE 語句與 REFERENCE 條件約束 “FK_Blogs_People_OwnerId” 衝突。 在資料庫「Scratch」的表「dbo.Blogs」的欄位'OwnerId'中發生衝突。 陳述式已經結束。

累計空值

可選的關聯包含參數屬性為 Null,可對應至資料庫欄的空值。 這表示當目前的主體/父系遭到刪除或從相依/子系中斷時,外鍵值可以設定為 null。

讓我們再看看 何時發生串連行為中的範例,但這次選擇性關聯由可為 Null 的 Post.BlogId 外鍵屬性來表示:

public int? BlogId { get; set; }

刪除相關部落格時,每個文章的這個外鍵屬性將會設定為 null。 例如,此程式代碼與之前相同:

using var context = new BlogsContext();

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

context.Remove(blog);

await context.SaveChangesAsync();

呼叫 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 = await context.Blogs.OrderBy(e => e.Name).Include(e => e.Posts).FirstAsync();

foreach (var post in blog.Posts)
{
    post.Blog = null;
}

await context.SaveChangesAsync();

或:

using var context = new BlogsContext();

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

blog.Posts.Clear();

await context.SaveChangesAsync();

然後,呼叫 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,幾乎一律會在使用 SQL Server 時產生關聯性迴圈。 如需設定層疊 Null 的詳細資訊,請參閱下一節。

設定串接行為

小提示

在來這裡之前,請務必閱讀上述章節。 如果無法瞭解上述材料,組態選項可能會沒有意義。

級聯行為根據OnDelete關聯性,使用OnModelCreating方法進行設定。 例如:

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 移轉或 OnDelete所建立之外鍵約束上每個EnsureCreated值的結果。

DeleteBehavior 對資料庫架構的影響
瀑布 ON DELETE CASCADE(在刪除時級聯)
限制 刪除時限制操作
無行動 資料庫預設值
設為空值 刪除時設為空值
ClientSetNull (用戶設定為空) 資料庫預設值
ClientCascade 資料庫預設值
客戶無行動 資料庫預設值

ON DELETE NO ACTION(資料庫預設值)和ON DELETE RESTRICT在關係資料庫中的行為通常是相同或非常相似。 儘管NO ACTION可能暗示有所不同,這兩個選項都會導致強制執行引用條件約束。 當存在差異時,這個差異在於資料庫檢查條件約束的時間。 檢查資料庫說明文件,瞭解資料庫系統上 ON DELETE NO ACTIONON DELETE RESTRICT 之間的特定差異。

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

唯一會導致資料庫串聯行為的值是 CascadeSetNull。 所有其他值都會將資料庫設定為不會重疊任何變更。

對 SaveChanges 行為的影響

下列各節中的表格說明當主體/父系被刪除,或其與相依/子實體的關聯被切斷時,相依/子實體會發生什麼事。 每個資料表涵蓋下列其中一個:

  • 選擇性 (可為 Null 的 FK) 和必要 (不可為 Null 的 FK) 關聯性
  • 當 DbContext 載入和追蹤相依/子系時,以及它們只存在於資料庫中時

載入與受扶養者/子女的必要關係

DeleteBehavior 刪除主體/父系時 從主體/父系進行分割
瀑布 EF Core 刪除的相依物件 EF Core 刪除的相依物件
限制 InvalidOperationException InvalidOperationException
無行動 InvalidOperationException InvalidOperationException
設為空值 SqlException 在建立資料庫時 SqlException 在建立資料庫時
ClientSetNull (用戶設定為空) InvalidOperationException InvalidOperationException
ClientCascade EF Core 刪除的相依物件 EF Core 刪除的相依物件
客戶無行動 DbUpdateException InvalidOperationException

注意事項:

  • 這類必要關聯性的預設值為 Cascade
  • 呼叫 SaveChanges 時,針對必要的關聯性使用級聯刪除以外的任何方法,將會導致例外狀況。
    • 一般而言,這是來自 EF Core 的 InvalidOperationException,因為在載入的子系/相依項中偵測到無效狀態。
    • ClientNoAction 會強制 EF Core 在將相依物件傳送至資料庫之前,不檢查關聯物件修正,因此在此情況下,資料庫會擲回例外狀況,然後由 SaveChanges 封裝在 DbUpdateException 中。
    • SetNull 在建立資料庫時會遭到拒絕,因為外鍵欄位不可設為 Null。
  • 由於相依關係/子項已載入,因此 EF Core 總是會刪除它們,而不會交由資料庫刪除。

未載入與受扶養人/子女的必要關係

DeleteBehavior 刪除主體/父系時 從主體/父系進行分割
瀑布 由資料庫刪除的相依項目 N/A
限制 DbUpdateException N/A
無行動 DbUpdateException N/A
設為空值 SqlException 在建立資料庫時 N/A
ClientSetNull (用戶設定為空) DbUpdateException N/A
ClientCascade DbUpdateException N/A
客戶無行動 DbUpdateException N/A

注意事項:

  • 由於未載入子女或受扶養人,因此在這裡斷絕關係並不有效。
  • 這類必要關聯性的預設值為 Cascade
  • 呼叫 SaveChanges 時,針對必要的關聯性使用級聯刪除以外的任何方法,將會導致例外狀況。
    • 通常,這是因為 DbUpdateException 中的相依/子節點未載入,因此資料庫只能偵測到無效的狀態。 SaveChanges 接著會將資料庫例外狀況包裝在DbUpdateException中。
    • SetNull 在建立資料庫時會遭到拒絕,因為外鍵欄位不可設為 Null。

載入可選關係的附屬物/子女

DeleteBehavior 刪除主體/父系時 從主體/父系進行分割
瀑布 EF Core 刪除的相依物件 EF Core 刪除的相依物件
限制 EF Core 將相依 FK 設定為 Null EF Core 將相依 FK 設定為 Null
無行動 EF Core 將相依 FK 設定為 Null EF Core 將相依 FK 設定為 Null
設為空值 EF Core 將相依 FK 設定為 Null EF Core 將相依 FK 設定為 Null
ClientSetNull (用戶設定為空) EF Core 將相依 FK 設定為 Null EF Core 將相依 FK 設定為 Null
ClientCascade EF Core 刪除的相依物件 EF Core 刪除的相依物件
客戶無行動 DbUpdateException EF Core 將相依 FK 設定為 Null

注意事項:

  • 這類選擇性關聯性的預設值為 ClientSetNull
  • 相依專案/子系永遠不會被刪除,除非已設定CascadeClientCascade
  • 所有其他值都會讓 EF Core 將相依 FK 設定為 Null...
    • 除了 ClientNoAction,這將告知 EF Core 在刪除主要實體/父項目時,不要更動依賴對象/子項目的外鍵。 因此,資料庫會擲回例外狀況,而該例外狀況會被 SaveChanges 封裝為 DbUpdateException

未載入受撫養人/子女的選擇性關係

DeleteBehavior 刪除主體/父系時 從主體/父系進行分割
瀑布 由資料庫刪除的相依項目 N/A
限制 DbUpdateException N/A
無行動 DbUpdateException N/A
設為空值 依資料庫將相依 FK 設定為 null N/A
ClientSetNull (用戶設定為空) DbUpdateException N/A
ClientCascade DbUpdateException N/A
客戶無行動 DbUpdateException N/A

注意事項:

  • 由於未載入子女或受扶養人,因此在這裡斷絕關係並不有效。
  • 這類選擇性關聯性的預設值為 ClientSetNull
  • 除非資料庫已設為級聯刪除或允許 Null 值,否則必須載入依賴/子項以避免資料庫異常狀態。