当一个实体类型的任意数目实体与相同或另一个实体类型的任意数量的实体相关联时,将使用多对多关系。 例如,一个 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
,而另一个是Tag
中包含其相关的Posts
的反向导航。 例如:
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 模型生成约定会将此处显示的Post
类型和Tag
类型映射到本部分顶部数据库架构中的三个表。 在不显式使用联接类型的情况下,此映射通常指的是“多对多”这一术语的涵义。
例子
以下部分包含多对多关系的示例,包括实现每个映射所需的配置。
小窍门
以下所有示例的代码都可以在 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
,并为已有的Post
和Tag
类型创建:
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>();
}
PostId
和TagId
会被自动选为外键,并配置为联接实体类型的复合主键。 对于与 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 需要进行一些配置以便确定这应该是多对多关系,而不是一对多关系。 这是使用 HasMany
和 WithMany
完成的,但没有在未导航的情况下在一侧传递任何参数。 例如:
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);
带候选键的多对多
到目前为止,所有示例都显示了联接实体类型中的外键被约束为关系两侧实体类型的主键。 可以改为将每个外键或两者约束为备用键。 例如,请考虑此模型,其中Tag
并 Post
具有备用键属性:
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);
也可以将主键添加到联接实体,而无需为其定义类。 例如,仅有 Post
和 Tag
类型:
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!;
}
这不需要特殊的映射,因为这些是具有正常 一对多 关系的普通实体类型。
其他资源
- .NET 数据社区例会,深入探讨多对多关系和其基础结构。