共用方式為


變更外鍵和導航設定

外鍵和導覽的概觀

Entity Framework Core (EF Core) 模型中的關聯性會使用外鍵 (FK) 來表示。 FK 是由關聯性中相依或子實體上的一或多個屬性所組成。 當相依/子系上的外鍵屬性值符合主體/父系上替代或主鍵 (PK) 屬性的值時,這個相依/子實體會與指定的主體/父實體相關聯。

外鍵是儲存及處理資料庫關聯的好方法,但在應用程式代碼中使用多個相關實體時不太方便。 因此,大部分 EF Core 模型也會在 FK 表示法上分層「導覽」。 導航會形成 C#/.NET 參考,在實體實例之間反映藉由將外鍵值比對至主鍵或替代鍵值所找到的關聯。

導覽可以用於關聯性的兩端、一端,或完全不能使用,只留下 FK 屬性。 FK 屬性可以藉由將它設為 陰影屬性來隱藏。 如需模型關聯性的詳細資訊,請參閱 關聯 性。

小提示

本文件假設您已瞭解實體狀態以及 EF Core 變更追蹤的基本概念。 如需這些主題的詳細資訊,請參閱 EF Core 中的變更追蹤

小提示

您可以從 GitHub 下載範例程式代碼,以執行並偵錯此檔案中的所有程式代碼。

範例模型

下列模型包含四個實體類型,其中具有關聯性。 程序代碼中的批注會指出哪些屬性是外鍵、主鍵和導覽。

public class Blog
{
    public int Id { get; set; } // Primary key
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

此模型中的三個關聯性如下:

  • 每個部落格可以有許多貼文(一對多):
    • Blog 是主體/父系。
    • Post 是相依/子系。 它包含 FK 屬性 Post.BlogId,其值必須符合 Blog.Id 相關部落格的 PK 值。
    • Post.Blog 是從文章到相關聯部落格的參考導覽。 Post.BlogBlog.Posts 的逆向導航。
    • Blog.Posts 是從部落格導覽至所有相關文章的集合。 Blog.PostsPost.Blog 的逆向導航。
  • 每個部落格都可以有一個資產(一對一):
    • Blog 是主體/父系。
    • BlogAssets 是相依/子系。 它包含 FK 屬性 BlogAssets.BlogId,其值必須符合 Blog.Id 相關部落格的 PK 值。
    • BlogAssets.Blog 是從資產到相關部落格的參照導航。 BlogAssets.BlogBlog.Assets 的逆向導航。
    • Blog.Assets 是從部落格到相關聯資產的參考導引。 Blog.AssetsBlogAssets.Blog 的逆向導航。
  • 每個帖子可以有許多標籤,每個標籤可以有許多帖子(多對多):
    • 多對多關係是兩個一對多關係的進一步發展。 本文件稍後將涵蓋多對多關係。
    • Post.Tags 是從貼文到所有相關聯標記的集合導覽。 Post.TagsTag.Posts 的逆向導航。
    • Tag.Posts 是從標籤導覽至所有相關聯文章的集合。 Tag.PostsPost.Tags 的逆向導航。

如需如何建立模型和設定關聯性的詳細資訊,請參閱 關聯 性。

關係調整

EF Core 會讓導覽與外鍵值保持一致,反之亦然。 也就是說,如果外鍵值變更,使其現在參考不同的主從實體,則會更新導覽屬性以反映這項變更。 同樣地,如果流覽已變更,則會更新涉及之實體的外鍵值,以反映這項變更。 這稱為「關聯修正」。

根據查詢進行修正

從資料庫查詢實體時,會先進行修正。 資料庫只有外鍵值,因此當EF Core 從資料庫建立實體實例時,它會使用外鍵值來設定參考導覽,並視需要將實體新增至集合導覽。 例如,請考慮查詢部落格及其相關聯的文章和資產:

using var context = new BlogsContext();

var blogs = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

針對每個部落格,EF Core 會先建立 Blog 實例。 然後,當每個文章從資料庫載入時,其 Post.Blog 參考瀏覽會設定為指向相關聯的部落格。 同樣地,貼文會新增至 Blog.Posts 集合導覽。 與 BlogAssets發生類似的情況,但在這種情況下,這兩個導覽都是參考。 導航功能Blog.Assets被設定為指向資產實例,而導航功能BlogAsserts.Blog則被設定為指向部落格實例。

檢視此查詢之後 的變更追蹤器偵錯檢視 會顯示兩個部落格,每個都有一個資產和兩篇文章被追蹤。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

除錯視圖會呈現鍵值和導覽。 導覽會使用相關實體的主鍵值來顯示。 例如,在上述輸出中, Posts: [{Id: 1}, {Id: 2}] 表示 Blog.Posts 集合導覽分別包含兩個具有主鍵 1 和 2 的相關文章。 同樣地,對於與第一個部落格相關聯的每篇文章,Blog: {Id: 1} 一行表示 Post.Blog 導覽會參考主鍵為 1 的部落格。

修復本地追蹤的實體

關聯性修正也會發生於透過追蹤查詢返回的實體與已由 DbContext 追蹤的實體之間。 例如,請考慮針對部落格、文章和資產執行三個不同的查詢:

using var context = new BlogsContext();

var blogs = await context.Blogs.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = await context.Assets.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = await context.Posts.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

再次查看偵錯檢視,在第一次查詢之後,只會追蹤兩個部落格:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Blog.Assets參考導覽為 Null,而Blog.Posts集合導覽是空的,因為內容目前沒有追蹤任何相關的實體。

在第二個查詢之後, Blogs.Assets 參考導覽已修正,以指向新追蹤的 BlogAsset 實例。 同樣地, BlogAssets.Blog 引用導航會設定為指向已追蹤的適當 Blog 實例。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

最後,第三個查詢之後, Blog.Posts 集合導覽現在會包含所有相關文章,而 Post.Blog 參考會指向適當的 Blog 實例:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

這與原始的單一查詢達成相同的最終狀態,因為 EF Core 修正了實體追蹤過程中的導航,在來自多個不同查詢時也是如此。

備註

修正永遠不會讓更多數據從資料庫傳回。 它僅連接已被查詢返回的實體或已被 DbContext 追蹤的實體。 如需在序列化實體時處理重複項目的相關資訊,請參閱 EF Core 中的身份解析

利用導航更改關係

若要變更兩個實體之間的關聯性,最簡單的方式是作導覽,同時讓EF Core 適當地修正反向流覽和 FK 值。 這可以透過:

  • 從集合導覽新增或移除實體。
  • 將參考導覽變更為指向不同的實體,或將它設定為 null。

在集合導航中新增或移除項目

例如,讓我們將其中一篇文章從 Visual Studio 部落格移至 .NET 部落格。 這需要先載入部落格和文章,然後將文章從一個部落格上的流覽集合移至另一個部落格上的流覽集合:

using var context = new BlogsContext();

var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == "Visual Studio Blog");

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

小提示

這裡需要呼叫 ChangeTracker.DetectChanges() ,因為存取偵錯檢視並不會導致 自動偵測變更

這是執行上述程式代碼之後所列印的偵錯檢視:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Blog.Posts.NET 部落格上的導航功能現在有三篇文章(Posts: [{Id: 1}, {Id: 2}, {Id: 3}])。 同樣地, Blog.Posts Visual Studio 部落格上的導覽只有一篇文章 (Posts: [{Id: 4}])。 這是預期,因為程式代碼已明確變更這些集合。

更有趣的是,即使程式代碼並未明確變更 Post.Blog 流覽,但已修正以指向Visual Studio部落格 (Blog: {Id: 1})。 此外, Post.BlogId 外鍵值已更新,以符合 .NET 部落格的主鍵值。 然後,呼叫 SaveChanges 時,FK 值的這項變更會保存至資料庫:

-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

變更參考導覽

在上一個範例中,透過操作每個部落格上的文章集合導覽,將文章從一個部落格移動到另一個部落格。 同樣的效果可以通過將Post.Blog 參考導航更改為指向新部落格來達成。 例如:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

此變更之後的偵錯檢視與上一個範例中的偵錯檢視 完全相同 。 這是因為 EF Core 偵測到參考導覽變更,然後修正集合導覽和 FK 值以符合。

使用外鍵值變更關聯設定

在上一節中,透過導覽操作關係,自動更新了外鍵值。 這是在 EF Core 中操縱關聯的推薦方式。 不過,您也可以直接操作外鍵值。 例如,我們可以藉由變更 Post.BlogId 外鍵值,將文章從一個部落格移至另一個部落格:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

請注意,這與變更參考流覽的方式非常類似,如上一個範例所示。

此更改後的偵錯視圖再度與前兩個範例的情況完全相同。 這是因為 EF Core 偵測到 FK 值的變更,然後調整參考和集合導覽以匹配。

小提示

每次關聯性變更時,請勿撰寫程式碼來操作所有導覽和外鍵值。 這類代碼更加複雜,並且必須在各種情況下確保對外鍵和導覽屬性的一致變更。 如果可能的話,只操控單一導覽,或者同時操控兩個導覽。 如有需要,只要調整 FK 值即可。 避免同時操控導航功能和外鍵值。

針對已新增或刪除實體的修正

添加至收藏導覽

EF Core 會在偵測 到新的相依/子實體已新增至集合導覽時,執行下列動作:

  • 如果該實體未被追蹤,則會開始追蹤。 (實體通常處於 Added 狀態。不過,如果實體類型設定為使用產生的索引鍵並設定主鍵值,則會在狀態中 Unchanged 追蹤實體。
  • 如果實體與不同的主體/父系相關聯,則會切斷該關聯性。
  • 實體會被與擁有集合導航的主要/父系相關聯。
  • 所有相關的實體都會修正導覽和外鍵值。

根據這個,我們可以看到,若要將文章從一個部落格移至另一個部落格,我們實際上不需要從舊的集合導覽中移除它,然後再將其新增至新的集合導覽。 因此,上述範例中的程式碼可以變更為下列所示:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

收件者:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

EF Core 看到文章已新增至新的部落格,並自動從第一個部落格的集合中移除。

從集合導覽中移除

從主體/父系的集合瀏覽中移除相依/子實體會導致與該主體/父系的關係被切斷。 接下來會發生什麼取決於關聯性是選擇性還是必要。

選擇性關聯性

根據預設,選擇性關聯性會將外鍵值設定為 null。 這表示相依/子系不再與 任何 主體/父系相關聯。 例如,讓我們載入部落格和文章,然後從 Blog.Posts 集合導覽中移除其中一篇文章:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

檢視此 變更之後的變更追蹤偵錯檢視 ,顯示:

  • Post.BlogId FK 已設定為 null (BlogId: <null> FK Modified Originally 1
  • Post.Blog參考導覽已設定為 null (Blog: <null>
  • 貼文已從 Blog.Posts 集合導覽中移除 (Posts: [{Id: 1}]
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

請注意,這則貼文 沒有 被標示為 Deleted。 它會標示為 Modified ,以便在呼叫 SaveChanges 時,資料庫中的 FK 值設定為 Null。

必需的關係

對於必要的關聯性,不允許將 FK 值設定為 null(通常不可能)。 因此,斷絕必要的關聯性表示相依/子實體必須重新父系至新的主體/父系,或在呼叫SaveChanges時從資料庫移除,以避免引用條件約束違規。 這稱為「刪除孤兒」,而且是 EF Core 中必要的關聯的預設行為。

例如,讓我們將部落格與文章之間的關聯性變更為必須,然後執行與上一個範例相同的程式碼:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

在進行此變更後查看除錯視圖顯示:

  • 當呼叫 SaveChanges 時,該貼文已標示為 Deleted,因此將從資料庫中刪除。
  • Post.Blog參考導覽已設定為 null (Blog: <null>)。
  • 貼文已從 Blog.Posts 集合導覽中移除(Posts: [{Id: 1}])。
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

請注意, Post.BlogId 會維持不變,因為對於必要的關聯性,它無法設定為 null。

呼叫 SaveChanges 會導致刪除孤立的貼文:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

刪除孤兒計時和重新父系

根據預設,一旦Deleted關聯性變更,就會標示孤立專案。 不過,此過程可以延遲,直到實際呼叫 SaveChanges 為止。 這有助於避免從一個主體/父系移除後的實體成為孤立,因為在呼叫SaveChanges之前,這些實體會重新被新的主體/父系接管。 ChangeTracker.DeleteOrphansTiming 是用來設定這個時間。 例如:

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

從第一個集合移除 post 之後,物件不會像先前範例一樣被標示為 Deleted。 相反地,EF Core 正在追蹤該關聯性被切斷的情況,即使這是必要的關聯。 即使 FK 值的類型不可為 Null,EF Core 還是會將其視為 Null,這通常被稱作「概念上的 Null」。

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

此時呼叫 SaveChanges 會導致刪除孤立的貼文。 不過,如果如上述範例所示,在呼叫 SaveChanges 之前,文章會與新的部落格相關聯,則它會適當地修正到該新部落格,不再被視為孤兒:

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

此時呼叫的 SaveChanges 將會更新資料庫中的貼文,而不是刪除它。

您也可以關閉自動刪除孤兒。 如果在追蹤孤立專案時呼叫 SaveChanges,這會導致例外狀況。 例如,此程式代碼:

var dotNetBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

await context.SaveChangesAsync(); // Throws

將會拋出此例外:

System.InvalidOperationException:實體 'Blog' 和 'Post' 的關聯已被拆除,鍵值為 '{BlogId: 1}',但此關係標示為必需或隱含必需,這是因為外部鍵不可為 Null。 如果應該在切斷必要關聯性時刪除相依/子實體,請將關聯性設定為使用串聯刪除。

您可以隨時呼叫 ChangeTracker.CascadeChanges()來強制刪除孤立資料,以及級聯刪除。 結合這個與將刪除孤立物件的時機設定為 Never ,可確保除非明確指示 EF Core 這樣做,否則永遠不會刪除孤立物件。

變更參考導覽

變更一對多關聯性的參考導覽的效果與變更關聯性另一端的集合導覽相同。 將相依/子系的參考瀏覽設定為 null,相當於從主體/父系的集合導覽中移除實體。 所有修正和資料庫變更都會如上一節所述進行,如果需要關聯性但無法滿足,將使實體成為孤立實體。

選擇性的一對一關係

若為一對一關聯性,變更引用導航會導致任何先前的關係被切斷。 對於選擇性關聯性,這表示先前相關相依/子系上的 FK 值會設定為 null。 例如:

using var context = new BlogsContext();

var dotNetBlog = await context.Blogs.Include(e => e.Assets).SingleAsync(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

呼叫 SaveChanges 之前的偵錯檢視顯示,新的資產已取代現有的資產,現有的資產現在被標示為 Modified 並且 FK 值是空值 BlogAssets.BlogId

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

這會在呼叫 SaveChanges 時產生更新和插入:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

必要的一對一關係

執行與上一個範例相同的程式碼,但這次要求必須維持一對一關聯,顯示先前關聯的 BlogAssets 現在被標示為 Deleted,因為當新的 BlogAssets 取代它的位置時,它會成為孤立的對象。

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

這會導致在呼叫 SaveChanges 時刪除和插入:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

將孤立項目標示為已刪除的時間可以與集合導覽顯示的方式相同,而且效果相同。

刪除實體

選擇性關聯性

當實體標示為 Deleted時,例如呼叫 DbContext.Remove,就會從其他實體的導覽中移除已刪除實體的參考。 針對選擇性關聯性,相依實體中的 FK 值會設定為 null。

例如,讓我們將Visual Studio部落格標示為 Deleted

using var context = new BlogsContext();

var vsBlog = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .SingleAsync(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

在呼叫 SaveChanges 之前查看 變更追蹤器偵錯檢視 會顯示:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

請注意:

  • 部落格標示為 Deleted
  • 與已刪除部落格相關的資產具有空 FK 值(BlogId: <null> FK Modified Originally 2)和空參考導覽(Blog: <null>
  • 與已刪除部落格相關的每個貼文都有 Null FK 值(BlogId: <null> FK Modified Originally 2)和空參考導覽(Blog: <null>

必需的關係

必要關聯性的修正行為與選擇性關聯性相同,不同之處在於相依/子實體會標示為 Deleted ,因為它們在沒有主體/父系的情況下無法存在,而且必須在呼叫 SaveChanges 時從資料庫移除,以避免引用條件約束例外狀況。 這稱為「串聯刪除」,而且是EF Core 中必要關聯性的默認行為。 例如,執行與上一個範例相同的程式代碼,但具有必需的關聯性,結果在呼叫 SaveChanges 之前會顯示出下列偵錯檢視:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

如預期般,附屬者/子女現在會標示為 Deleted。 不過,請注意,已刪除實體上的導覽 尚未 變更。 這看起來可能很奇怪,但它可藉由清除所有導覽來避免完全粉碎已刪除的實體圖表。 也就是說,即使刪除了,部落格、資產和文章仍會形成一個實體圖形。 這可讓您更輕鬆地復原刪除實體圖,與在 EF6 中圖形被分解的情況相比。

級聯刪除時機和重新指定父系

根據預設,當父/主體標示為 Deleted時,就會立即發生串聯刪除。 這與刪除孤兒相同,如先前所述。 如同刪除孤立資料,此過程可能會延遲到呼叫 SaveChanges,甚至可以透過適當設定 ChangeTracker.CascadeDeleteTiming 完全停用。 這與刪除孤兒的方式相同,包括刪除主體/父系之後重新養育子系/相依專案。

串聯刪除,以及刪除孤立項目,隨時都可以藉由呼叫 ChangeTracker.CascadeChanges()來強制。 將此設定與 Never 的串聯刪除時機結合,能確保除非明確指示 EF Core 執行,否則永遠不會發生串聯刪除。

小提示

串聯刪除和刪除孤立專案密切相關。 這兩者都會在與必要主體/父系的關聯性遭到切斷時刪除相依/子實體。 如果是級聯刪除,此動作會發生,因為主要/父項自身被刪除。 針對孤立對象,主體/父實體仍然存在,但不再與相依/子實體相關。

多對多關聯性

在 EF Core 中,多對多關聯性是通過使用聯結實體來實作的。 多對多關聯性的每一端都透過一對多關係連接到這個聯結實體。 這個連接實體可以明確地定義和映射,也可以隱含地創建並隱藏。 在這兩種情況下,基礎行為都相同。 我們會先查看此底層行為,以瞭解多對多關聯性的追蹤如何運作。

多對多關係的運作方式

請考慮使用明確定義的聯結實體類型,在貼文和標記之間建立多對多關聯性的 EF Core 模型:

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 IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

請注意, PostTag 聯結實體類型包含兩個外鍵屬性。 在此模型中,對於與標記相關的貼文,必須有一個 PostTag 聯結實體,其中PostTag.PostId外鍵值與Post.Id主鍵值匹配,且PostTag.TagId外鍵值與Tag.Id主鍵值匹配。 例如:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

在執行此程式代碼之後查看 變更追蹤器偵錯檢視 ,顯示貼文和標記與新的 PostTag 聯結實體相關:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

請注意,PostTag上的集合導覽已修正,以及PostTag上的參考導覽。 這些關聯性可以透過導覽而非 FK 值來作,就像上述所有範例一樣。 例如,您可以藉由在聯結實體上設定參考導覽,修改上述程式代碼以新增關聯性:

context.Add(new PostTag { Post = post, Tag = tag });

這會產生與上一個範例中完全相同的外鍵 (FK) 和導覽屬性變更。

略過導覽

手動操作關聯表會很繁瑣。 您可以使用「略過」聯結實體的特別集合導覽,直接操控多對多關聯性。 例如,可以將兩個跳過導覽新增至上述模型。一個從貼文到標籤,另一個則從標籤到貼文。

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 IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

此多對多關聯性需要下列設定,以確保略過導覽和一般導覽全都用於相同的多對多關聯性:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

如需有關映射多對多關聯性的詳細資訊,請參閱關聯性

略過導覽的外觀和行為就像一般集合導覽。 不過,處理外鍵值的方式不同。 讓我們將貼文與標記產生關聯,但這次使用跳過導覽功能:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

請注意,此程式代碼不會使用聯結實體。 其實,它只是將一個實體新增到導覽集合中,就如同在一對多關係中會採取的方式一樣。 產生的偵錯檢視基本上與之前相同:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

請注意,已自動建立聯結實體的 PostTag 實例,並將 FK 值設定為標記和貼文的 PK 值,而該值現在已相關聯。 所有一般參考和集合導覽都已修正,以符合這些 FK 值。 此外,由於此模型包含跳過導航連結,因此這些問題也已修正。 具體來說,即使我們已將標記添加到 Post.Tags 略過導覽,Tag.Posts 這一關聯性另一邊的反向略過導覽也已修正,以包含相關的貼文。

值得注意的是,即使已增加跳過的瀏覽,仍然可以直接操作基礎的多對多關聯性。 例如,標籤和文章可以如同在介紹略過導覽功能之前那樣相關聯:

context.Add(new PostTag { Post = post, Tag = tag });

或使用 FK 值:

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

這樣做仍然可以正確處理跳過導航問題,使偵錯檢視的輸出與上一個範例相同。

僅略過導航項目

在上一節中,我們除了完整定義兩個基礎的一對多關聯性之外,還新增了跳過導覽。 這很適合用來說明 FK 值會發生什麼情況,但通常不需要。 反而,可以使用略過導覽來定義多對多關係。 這是本檔頂端的模型中定義多對多關聯性的方式。 使用此模型,我們可以藉由將貼文新增至 Tag.Posts 略過導覽來再次建立關聯(或者,將標籤新增至 Post.Tags 略過導覽):

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

在進行這項變更之後查看偵錯檢視,顯示EF Core已建立 Dictionary<string, object> 實例來表示聯結實體。 這個聯結實體同時包含 PostsIdTagsId 外鍵屬性,這些屬性已設定為符合相關聯之 post 和標記的 PK 值。

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

如需有關隱含聯結實體與實體類型使用的詳細資訊,請參閱關聯Dictionary<string, object>

這很重要

依慣例用於聯結實體類型的 CLR 類型在未來版本中可能會變更,以改善效能。 除非已明確設定,否則請勿相依於聯結類型 Dictionary<string, object>

結合具有有效負載的物件

到目前為止,所有範例都使用了聯結實體類型(無論是明確或隱含的),該類型僅包含多對多關聯性所需的兩個外鍵屬性。 在作關聯性時,應用程式不需要明確設定這兩個 FK 值,因為它們的值來自相關實體的主鍵屬性。 這可讓EF Core 建立聯結實體的實例,而不需要遺失數據。

具有生成值的載荷

EF Core 支援將其他屬性新增至聯結實體類型。 這稱為授與聯結實體「有效負載」。 例如,讓我們將TaggedOn屬性新增到聯結PostTag實體:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

當 EF Core 建立聯結實體實例時,將不會設定這個承載屬性。 最常見的處理方法是使用具有自動產生值的承載屬性。 例如,當插入每個新實體時, TaggedOn 屬性可以設定為使用儲存產生的時間戳:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

您現在可以以與之前相同的方式標記文章:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

在呼叫 SaveChanges 之後查看 變更追蹤器偵錯檢視 ,顯示已適當地設定承載屬性:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

明確設定承載值

在上述範例中,讓我們新增不會使用自動產生值的承載屬性:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

您現在可以用與之前相同的方式標記文章,並且系統仍然會自動建立關聯實體。 接著可以使用 存取追蹤實體中所述的其中一個機制來存取此實體。 例如,下列程式代碼會使用 DbSet<TEntity>.Find 來存取聯結實體實例:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = await context.Set<PostTag>().FindAsync(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

一旦找到聯結實體,就可以以正常的方式進行操作。在此範例中,您可以在呼叫 SaveChanges 之前設定 TaggedBy 承載屬性。

備註

請注意,在這裡需要呼叫 ChangeTracker.DetectChanges() ,才能讓EF Core有機會偵測巡覽屬性變更,並在 Find 使用之前建立聯結實體實例。 如需詳細資訊 ,請參閱變更偵測和通知

或者,可以明確建立聯結實體,以將貼文與標記產生關聯。 例如:

using var context = new BlogsContext();

var post = context.Posts.SingleAsync(e => e.Id == 3);
var tag = context.Tags.SingleAsync(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

最後,設定承載數據的另一種方式是先覆寫 SaveChanges 或使用 DbContext.SavingChanges 事件來處理實體,再更新資料庫。 例如:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return await base.SaveChangesAsync(cancellationToken);
}