Share via


多對多關聯性

當一個實體類型的任意數目實體與相同或另一個實體類型的任意數目實體相關聯時,就會使用多對多關聯性。 例如, Post 可以有許多相關聯的 Tags,而且每個 Tag 都可以與任意數目 Posts產生關聯。

瞭解多對多關聯性

多對多關聯性與 一對多一對一 關聯性不同,因為它們不能以簡單的方式使用外鍵來表示。 相反地,需要額外的實體類型,才能「聯結」關聯性的兩端。 這稱為「聯結實體類型」,並對應至關係資料庫中的「聯結數據表」。 這個聯結實體類型的實體包含一組外鍵值,其中一對指向關聯性一邊的實體,另一對指向關聯性另一端的實體。 每個聯結實體,因此聯結數據表中的每個數據列都代表關聯性中實體類型之間的一個關聯。

EF Core 可以隱藏聯結實體類型,並在幕後管理它。 這可讓多對多關聯性的導覽以自然方式使用,並視需要新增或移除每個端的實體。 不過,瞭解幕後發生的情況,使其整體行為,特別是關係資料庫對應相當有用。 讓我們從關係資料庫架構設定開始,以代表文章和標記之間的多對多關聯性:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

在此架構中, PostTag 是聯結數據表。 它包含兩個數據行: PostsId,這是數據表主鍵的 Posts 外鍵,而 TagsId這是數據表主鍵的 Tags 外鍵。 因此,此數據表中的每個數據列都代表一 Post 到一 Tag之間的關聯。

EF Core 中這個架構的簡單對應是由三個實體類型所組成,每個數據表各有一種。 如果每個實體類型都以 .NET 類別表示,這些類別可能會如下所示:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

請注意,在此對應中,沒有多對多關聯性,而是兩個一對多關聯性,一個用於聯結數據表中定義的每個外鍵。 這不是對應這些數據表的不合理方式,但不會反映聯結數據表的意圖,也就是代表單一多對多關聯性,而不是兩個一對多關聯性。

EF 允許透過引進兩個集合導覽來取得更自然的對應,一個是Post包含其相關 Tags,另一個是包含其相關 PostsTag 例如:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

提示

這些新的導覽稱為「略過導覽」,因為它們會略過聯結實體,以提供多對多關聯性另一端的直接存取權。

如下列範例所示,可以透過這種方式對應多對多關聯性,也就是聯結實體的 .NET 類別,以及兩個一對多關聯 性的導覽,以及 略過實體類型上公開的導覽。 不過,EF 可以透明地管理聯結實體,而不需要為其定義 .NET 類別,而且不需要流覽兩個一對多關聯性。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

事實上,根據預設,EF 模型建置慣例會將此處顯示的和 Tag 類型對應Post至本節頂端資料庫架構中的三個數據表。 這個對應沒有明確使用聯結類型,就是「多對多」一詞通常的意義。

範例

下列各節包含多對多關聯性的範例,包括達成每個對應所需的設定。

提示

您可以在 ManyToMany.cs 中找到下列所有範例的程式代碼。

基本多對多

在多對多的最基本案例中,關聯性每一端的實體類型都有集合導覽。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

此關聯性會 依慣例對應。 即使不需要,此關聯性的對等明確設定仍會顯示為學習工具:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

即使有此明確設定,仍會依照慣例來設定關聯性的許多層面。 為了學習目的,更完整的明確設定如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

重要

請不要嘗試完全設定所有專案,即使不需要的話。 如上所示,程式代碼會很快變得複雜,而且很容易犯錯。 即使在上述範例中,模型中仍有許多仍依慣例設定的專案。 認為 EF 模型中的所有專案一律可以明確設定是不現實的。

不論關聯性是由慣例所建置,還是使用其中一個顯示的明確組態,產生的對應架構(使用 SQLite) 為:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

使用 Database First 流程從 現有的資料庫建構 DbContext 時,EF Core 6 和更新版本會在資料庫架構中尋找此模式,並建立多對多關聯性,如本檔所述。 此行為可透過使用 自定義 T4 範本來變更。 如需其他選項,請參閱 沒有對應聯結實體的多對多關聯性現在已建立 Scaffold

重要

目前,EF Core 會使用 Dictionary<string, object> 來表示尚未設定 .NET 類別的聯結實體實例。 不過,為了改善效能,未來 EF Core 版本可能會使用不同的類型。 除非已明確設定,否則請勿相依於聯結類型 Dictionary<string, object>

具有具名聯結數據表的多對多

在上一個範例中,聯結數據表是以慣例命名 PostTag 。 它可以使用 來指定明確的名稱 UsingEntity。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

對應的其他所有項目都會維持不變,只有聯結數據表的名稱會變更:

CREATE TABLE "PostsToTagsJoinTable" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有聯結數據表外鍵名稱的多對多

在上一個範例中,聯結數據表中的外鍵數據行名稱也可以變更。 做法有二種。 第一個是明確指定聯結實體上的外鍵屬性名稱。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

第二種方式是讓屬性保留其依慣例名稱,但然後將這些屬性對應至不同的數據行名稱。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.Property("PostsId").HasColumnName("PostForeignKey");
                j.Property("TagsId").HasColumnName("TagForeignKey");
            });
}

不論是哪一種情況,對應都保持不變,只有外鍵數據行名稱已變更:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

雖然這裡未顯示,但可以合併上述兩個範例,以對應變更聯結數據表名稱和其外鍵數據行名稱。

具有聯結實體類別的多對多

到目前為止,在範例中,聯結數據表已自動對應至 共用類型實體類型。 這樣就不需要為實體類型建立專用類別。 不過,讓這類類別能夠輕鬆參考,特別是當導覽或承載新增至類別時,這非常有用,如下列稍後範例所示。 若要這樣做,請先建立聯結實體的類型PostTag,以及 和Tag的現有類型Post

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

提示

類別可以有任何名稱,但通常會在關聯性任一端結合型別的名稱。

UsingEntity現在,方法可以用來將此設定為關聯性的聯結實體類型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

PostIdTagId 會自動挑選為外鍵,並設定為聯結實體類型的複合主鍵。 針對不符合EF慣例的情況,可以明確設定要用於外鍵的屬性。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

此範例中聯結數據表的對應資料庫架構在結構上相當於先前的範例,但具有一些不同的數據行名稱:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有聯結實體導覽的多對多

在上一個範例之後,現在有代表聯結實體的類別,因此很容易新增參考這個類別的導覽。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

重要

如此範例所示,除了多對多關聯性兩端之間的略過導覽之外,還可以使用聯結實體類型的導覽。 這表示略過導覽可用來以自然方式與多對多關聯性互動,而當需要更充分地控制聯結實體本身時,就可以使用聯結實體類型的導覽。 從某種意義上說,此對應在簡單多對多對應之間提供兩個世界的最佳對應,以及更明確地符合資料庫架構的對應。

呼叫中 UsingEntity 不需要變更任何專案,因為依慣例會挑選聯結實體的導覽。 因此,此範例的組態與上一個範例的組態相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

巡覽可以明確設定為無法由慣例決定的情況。 例如:

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

對應的資料庫架構不會受到在模型中包含導覽的影響:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

多對多與聯結實體的導覽

上述範例會在多對多關聯性的任一端,將導覽新增至實體類型的聯結實體類型。 您也可以將導覽新增到其他方向或雙向。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

呼叫中 UsingEntity 不需要變更任何專案,因為依慣例會挑選聯結實體的導覽。 因此,此範例的組態與上一個範例的組態相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

巡覽可以明確設定為無法由慣例決定的情況。 例如:

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

對應的資料庫架構不會受到在模型中包含導覽的影響:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有導覽和變更外鍵的多對多

上一個範例顯示多對多,其中會巡覽至聯結實體類型,以及從聯結實體類型來回流覽。 這個範例相同,不同之處在於所使用的外鍵屬性也會變更。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostForeignKey { get; set; }
    public int TagForeignKey { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

同樣地,方法 UsingEntity 會用來設定下列專案:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

對應的資料庫架構現在是:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

單向多對多

注意

EF Core 7 中引進了單向多對多關聯性。 在舊版中,私人導覽可用來作為因應措施。

不需要在多對多關係兩側包含導覽。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

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

EF 需要一些設定才能知道這應該是多對多關聯性,而不是一對多關係。 這會使用 HasManyWithMany來完成,但沒有在側邊傳遞沒有導覽的自變數。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

移除導覽不會影響資料庫架構:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

多對多和聯結數據表與承載

在到目前為止的範例中,聯結數據表只用來儲存代表每個關聯的外鍵組。 不過,它也可以用來儲存關聯的相關信息,例如建立關聯的時間。 在這種情況下,最好定義聯結實體的類型,並將「關聯承載」屬性新增至此類型。 除了用於多對多關聯性的「略過導覽」之外,建立聯結實體的流覽也很常見。 這些額外的導覽可讓聯結實體輕鬆地從程式代碼參考,進而方便讀取和/或變更承載數據。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public DateTime CreatedOn { get; set; }
}

對於承載屬性,也常使用產生的值,例如,插入關聯數據列時自動設定的資料庫時間戳。 這需要一些最少的設定。 例如:

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

結果會對應至插入數據列時自動設定時間戳的實體類型架構:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

此處顯示的 SQL 適用於 SQLite。 在 SQL Server/Azure SQL 上,使用 .HasDefaultValueSql("GETUTCDATE()") 和 來 TEXT 讀取 datetime

自定義共用類型實體類型做為聯結實體

上述範例使用 型 PostTag 別做為聯結實體類型。 此類型專屬於貼文標記關聯性。 不過,如果您有多個具有相同圖形的聯結數據表,則相同的CLR類型可以用於所有資料表。 例如,假設我們所有的聯結數據表都有一個數據行 CreatedOn 。 我們可以使用對應為共用類型實體類型的類別來對應這些JoinType類別:

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

然後,您可以透過多個不同的多對多關聯性,將此類型參考為聯結實體類型。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Blog
{
    public int Id { get; set; }
    public List<Author> Authors { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

public class Author
{
    public int Id { get; set; }
    public List<Blog> Blogs { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

然後,您可以適當地設定這些關聯性,將聯結類型對應至每個關聯性的不同數據表:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Authors)
        .WithMany(e => e.Blogs)
        .UsingEntity<JoinType>(
            "BlogAuthor",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

這會導致資料庫架構中的下表:

CREATE TABLE "BlogAuthor" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);


CREATE TABLE "PostTag" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有替代索引鍵的多對多

到目前為止,所有範例都顯示聯結實體類型中的外鍵受限於關聯性任一端實體類型的主鍵。 每個外鍵或兩者都可以改為限制為替代索引鍵。 例如,請考慮此模型,其中TagPost 具有替代索引鍵屬性:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
}

此模型的組態如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

此外,為了清楚起見,產生的資料庫架構也會包含具有替代索引鍵的數據表:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostsAlternateKey" INTEGER NOT NULL,
    "TagsAlternateKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
    CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

如果聯結實體類型是以 .NET 類型表示,則使用替代索引鍵的組態會稍有不同。 例如:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

組態現在可以使用泛型 UsingEntity<> 方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

產生的架構如下:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

多對多和聯結數據表與個別主鍵

到目前為止,所有範例中的聯結實體類型都有由兩個外鍵屬性所組成的主鍵。 這是因為這些屬性的每個值組合最多可以發生一次。 因此,這些屬性會形成自然主鍵。

注意

EF Core 不支援任何集合導覽中的重複實體。

如果您控制資料庫架構,則聯結數據表沒有理由有額外的主鍵數據行,不過,現有的聯結數據表可能會定義主鍵數據行。 EF 仍然可以使用某些組態來對應至此設定。

建立類別來表示聯結實體,或許最容易達到這個。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }
}

這個 PostTag.Id 屬性現在會依慣例挑選為主鍵,因此唯一需要的組態是呼叫 UsingEntityPostTag 類型:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

聯結資料表產生的架構為:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

主鍵也可以新增至聯結實體,而不需為其定義類別。 例如,使用 just PostTag 類型:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

此組態可以新增金鑰:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.IndexerProperty<int>("Id");
                j.HasKey("Id");
            });
}

這會導致具有個別主鍵數據行的聯結數據表:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

多對多但不串聯刪除

在上述所有範例中,聯結數據表與多對多關聯性兩端之間建立的外鍵會使用 串聯刪除 行為來建立。 這非常有用,因為它表示如果刪除關聯性任一端的實體,則該實體的聯結數據表中的數據列會自動刪除。 或者,換句話說,當實體已不存在時,其與其他實體的關聯性也不再存在。

當變更此行為很有用時,很難想像,但可以視需要完成。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

聯結數據表的資料庫架構會在外鍵條件約束上使用受限制的刪除行為:

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);

自我參考多對多

相同的實體類型可以在多對多關聯性的兩端使用;這稱為「自我參考」關聯性。 例如:

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

這會對應至稱為 PersonPerson的聯結數據表,兩個外鍵都 People 指向數據表:

CREATE TABLE "PersonPerson" (
    "ChildrenId" INTEGER NOT NULL,
    "ParentsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
    CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);

對稱自我參考多對多

有時候多對多關聯性自然是對稱的。 也就是說,如果實體 A 與實體 B 相關,則實體 B 也會與實體 A 相關。這自然會使用單一導覽進行模型化。 例如,假設是人員 A 是與人員 B 的朋友,則人員 B 是具有人員 A 的朋友:

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

不幸的是,這並不容易對應。 同一個導覽不能用於關聯性的兩端。 最好的作法是將它對應為單向多對多關聯性。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

不過,為了確保兩個人彼此相關,每個人員都必須手動新增至對方的 Friends 集合。 例如:

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

直接使用聯結數據表

上述所有範例都會使用 EF Core 多對多對應模式。 不過,您也可以將聯結數據表對應至一般實體類型,並只針對所有作業使用兩個一對多關聯性。

例如,這些實體類型代表兩個一般數據表和聯結數據表的對應,而不使用任何多對多關聯性:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

這不需要特殊對應,因為這些是具有一對多關聯性的一般實體類型。

其他資源