變更外鍵和導覽

外鍵和導覽的概觀

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.Blog 是 的 Blog.Posts 反向流覽。
    • Blog.Posts 是從部落格流覽至所有相關聯文章的集合。 Blog.Posts 是 的 Post.Blog 反向流覽。
  • 每個部落格都可以有一個資產(一對一):
    • Blog 是主體/父系。
    • BlogAssets 是相依/子系。 它包含 FK 屬性 BlogAssets.BlogId ,其值必須符合 Blog.Id 相關部落格的 PK 值。
    • BlogAssets.Blog 是從資產到相關聯部落格的參考導覽。 BlogAssets.Blog 是 的 Blog.Assets 反向流覽。
    • Blog.Assets 是從部落格到相關聯資產的參考流覽。 Blog.Assets 是 的 BlogAssets.Blog 反向流覽。
  • 每個文章可以有許多標籤,每個標記可以有許多文章(多對多):
    • 多對多關聯性是兩個一對多關聯性的進一層。 本檔稍後涵蓋多對多關聯性。
    • Post.Tags 是從貼文到所有相關聯標記的集合導覽。 Post.Tags 是 的 Tag.Posts 反向流覽。
    • Tag.Posts 是從標籤巡覽至所有相關聯文章的集合。 Tag.Posts 是 的 Post.Tags 反向流覽。

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

關聯性修正

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

依查詢修正

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

using var context = new BlogsContext();

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

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 = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

var posts = context.Posts.ToList();
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 = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(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);

context.SaveChanges();

提示

這裡需要呼叫 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 中操作關聯性的建議方式。 不過,您也可以直接操作 FK 值。 例如,我們可以藉由變更 Post.BlogId 外鍵值,將文章從一個部落格移至另一個部落格:

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

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

此變更之後的偵錯檢視與 前兩個範例的情況完全相同。 這是因為 EF Core 偵測到 FK 值變更,然後修正要比對的參考和集合導覽。

提示

每次關聯性變更時,請勿撰寫程式碼來操作所有導覽和 FK 值。 這類程式碼更為複雜,而且必須確保每個情況下對外鍵和流覽的一致變更。 可能的話,只要操作單一導覽,或兩個導覽。 如有需要,只要操作 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.BlogIdFK 已設定為 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);

context.SaveChanges();

從第一個集合移除 post 之後,物件不會標示為 Deleted 先前範例中的貼文。 相反地,EF Core 會追蹤即使這是必要的關聯性,還是會 切斷關聯性 。 (即使 EF Core 無法真正為 Null,FK 值仍被視為 Null,因為類型不可為 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 = context.Blogs.Include(e => e.Posts).Single(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);

context.SaveChanges(); // Throws

將會擲回此例外狀況:

System.InvalidOperationException:實體 'Blog' 和 'Post' 與索引鍵值 '{BlogId: 1}' 之間的關聯已遭到切斷,但關聯性會標示為必要或隱含必要,因為外鍵不可為 Null。 如果應該在切斷必要關聯性時刪除相依/子實體,請將關聯性設定為使用串聯刪除。

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

變更參考導覽

變更一對多關聯性的參考流覽,的效果與變更關聯性另一端的集合導覽相同。 將相依/子系的參考流覽設定為 null,相當於從主體/父系的集合導覽中移除實體。 所有修正和資料庫變更都會如上一節所述發生,包括如果需要關聯性,讓實體成為孤立專案。

選擇性的一對一關聯性

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

using var context = new BlogsContext();

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

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

context.SaveChanges();

呼叫 SaveChanges 之前的偵錯檢視會顯示新資產已取代現有的資產,而現有資產現在標示為 Modified Null BlogAssets.BlogId FK 值:

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 = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

context.SaveChanges();

在呼叫 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
  • 與已刪除部落格相關的資產具有 Null FK 值 ( BlogId: <null> FK Modified Originally 2 ) 和 Null 參考流覽 ( Blog: <null>
  • 與已刪除部落格相關的每個文章都有 Null FK 值 ( BlogId: <null> FK Modified Originally 2 ) 和 Null 參考流覽 ( 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 主鍵值,以及外鍵值符合 Tag.Id 主鍵值的位置 PostTag.TagId 。 例如:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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}]

請注意,和 Tag 上的 Post 集合導覽已修正,如同 上的 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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 但此關聯性另一邊的反向略過流覽也已修正,以包含相關聯的貼文。

值得注意的是,即使略過導覽已分層,基礎多對多關聯性仍可以直接操作。 例如,標記和 Post 可以像我們在介紹略過導覽之前所做的一樣相關聯:

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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

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

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

注意

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

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

using var context = new BlogsContext();

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

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

context.SaveChanges();

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

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

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}