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 = 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.CascadeDeleteTiming和ChangeTracker.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 條件約束。
有兩種方式可以處理這種情況:
- 將一或多個關聯性變更為不串聯刪除。
- 配置資料庫時不包含一或多個級聯刪除功能,然後確保已載入所有相依實體,以便 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 ACTION 和 ON DELETE RESTRICT 之間的特定差異。
SQL Server 不支援 ON DELETE RESTRICT,因此 ON DELETE NO ACTION 會改用 。
唯一會導致資料庫串聯行為的值是 Cascade 和 SetNull。 所有其他值都會將資料庫設定為不會重疊任何變更。
對 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 的
- 由於相依關係/子項已載入,因此 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。 - 相依專案/子系永遠不會被刪除,除非已設定
Cascade或ClientCascade。 - 所有其他值都會讓 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 值,否則必須載入依賴/子項以避免資料庫異常狀態。