多对多关系

当一个实体类型的任意数量的实体与相同或另一个实体类型的任意数量的实体相关联时,将使用多对多关系。 例如,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 允许通过引入两个集合导航实现更自然的映射:一个在包含其相关的 TagsPost 上,另一个在包含其相关的 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 模型生成约定会将此处显示的 PostTag 类型映射到本节顶部的数据库架构中的三个表。 此映射(不显式使用联接类型)通常是指术语“多对多”。

示例

以下部分包含多对多关系的示例,包括实现每个映射所需的配置。

提示

可在 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 模板更改此行为。 有关其他选项,请参阅无映射联接实体的多对多关系现已搭建基架

重要

目前,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 的现有类型之外,首先为联接实体创建类型 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; } = [];
}

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 属性按约定选取为主键,因此,需要的唯一配置是调用 PostTag 类型的 UsingEntity

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);

还可以将主键添加到联接实体,而无需为其定义类。 例如,仅使用 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!;
}

这不需要特殊的映射,因为这些是具有正常一对多关系的正常实体类型。

其他资源