串聯刪除

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 = 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 條件約束。

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

  1. 將一或多個關聯性變更為不串聯刪除。
  2. 設定資料庫時沒有一或多個這些串聯刪除,然後確定已載入所有相依實體,讓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 會改用 。

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

對 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,否則必須載入相依/子系以避免資料庫例外狀況。