自引用关系
在一对多关系中,还有一种特殊的情况,就是 Dependent 和 Principal 都使用同一个实体类,即一个对象即是某个对象的 Dependent 之一,又是另一些对象的 Principal。这样的关系称作自引用关系,在结构上就是一个多叉树,其中根节点作为最高层次的 Principal,它只有 Dependent。
多叉树关系的表示有很多,最常用的是孩子表示法和父亲表示法。
孩子表示法,即每个实体都拥有一个集合引用,指向它的孩子(即 Dependent),没有单体引用,也没有外键。
class Unit { public Guid Id { get; set; } public string Name { get; set; } public List<Unit> Units { get; set; } = new List<Unit>(); }
父亲表示法,即每个实体都拥有一个单体引用,指向它的父亲(即 Principal),没有集合引用,外键既可以显式指定,也可以省略。
class Unit { public Guid Id { get; set; } public string Name { get; set; } public Guid ParentId { get; set; } //可省略 public Unit Parent { get; set; } }
在实体配置上,自引用关系和其他的一对多关系都是一致的。
一对一、多对多
一对一、多对多关系的实现和一对多关系差不多,区别只是在于使用 HasOne/HasMany 还是 WithOne/WithMany 而已。在前面已经说了,HasOne/HasMany 是用来指明实体关系下当前实体的导航属性,而 WithOne/WithMany 是用来指明实体关系下对方实体的导航属性。
在一对多关系中:Dependent 拥有单体引用,配置时使用 HasOne().WithMany() 而 Principal 拥有集合引用,配置时使用 HasMany().WithOne()
在一对一关系中,双方实体都只有单体引用,无论在哪一方配置,都是使用 HasOne().WithOne()
在多对多关系中,双方实体都只有集合引用,无论在哪一方配置,都是使用 HasMany().WithMany()
在一对多关系中,EFcore 可以通过导航属性判断出拥有集合引用的一端为 Principal,拥有单体引用的一端为 Dependent。
但在一对一关系中,EFCore 并不能通过导航属性类型来判断哪端作为 Dependent(即拥有外键的那一端),也就不知道在哪个数据表中添加外键列。这个时候就必须通过 HasForeignKey 方法来指明,此时和一对多关系不同,我们需要使用泛型方法,传入即将作为 Dependent 的实体类,并且在该类中,必须显式指定外键,而不能省略。
以购物单 Buy 和销售单 Sell 的一对一关系为例,其中外键将在 Buy 实体对应的数据表中生成:
class Sell { public Guid Id { get; set; } public string CommodityName { get; set; } public double Price { get; set; } public Buy Buy { get; set; } } class Buy { public Guid Id { get; set; } public string CommodityName { get; set; } public double Pay { get; set; } public Guid SellId { get; set; } public Sell Sell { get; set; } } class BuyConfig : IEntityTypeConfiguration<Buy> { public void Configure(EntityTypeBuilder<Buy> builder) { builder.ToTable("T_Buys"); builder.HasOne<Sell>(e => e.Sell).WithOne(e => e.Buy).HasForeignKey<Buy>(e => e.SellId); } }
在多对多关系中,双方实体都拥有集合引用,但 EFCore 不会纠结外键在哪个表生成了,因为多对多关系仅凭一列外键是不可能表示的。多对多关系需要第三张表来描述实体之间的对应关系,这个表由两列组成,分别是双方实体的 Id ,每一行数据都表示它们之间拥有关系。
注:多对多关系虽然不能通过一列外键表示,但是可以通过两列外键表示,本质就是两个一对多关系的复合,但 EFCore 的默认行为是通过第三张表来表示的。关于两列外键的实现方式这里不述。
EFCore 会自动生成并维护这个第三张表,我们只需要配置好实体即可。修改 Book 和 Author 实体,使其形成多对多关系(一本书可能有多个作者,一个作者可能有多本著作):
public class Book { public long Id { get; set; } public string Title { get; set; } public DateTime Time { get; set; } public double Price { get; set; } public List<Author> Authors { get; set; } = new List<Author>(); } public class Author { public int Id { get; set; } public string Name { get; set; } public List<Book> Books { get; set; } = new List<Book>(); } class BookConfig : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> builder) { builder.ToTable("T_Books"); builder.HasMany<Author>(e => e.Authors).WithMany(e => e.Books); } }
此时 EFCore 会生成一个名为 AuthorBook 的表,它就是表示多对多关系的关联表。下面演示一下多对多关系插入数据:
static async Task Main(string[] args) { using (MyDbContext ctx = new MyDbContext()) { Book[] books = new Book[] { new Book{ Title = "红楼梦", Price = 20, Time = DateTime.Now}, new Book{ Title = "黑暗的左手", Price = 20, Time = DateTime.Now}, new Book{ Title = "一无所有的人", Price = 20, Time = DateTime.Now} }; Author[] authors = new Author[] { new Author{ Name = "曹雪芹"}, new Author{ Name = "厄休拉"}, new Author{ Name = "无名氏"} }; books[0].Authors.Add(authors[0]); books[0].Authors.Add(authors[2]); authors[1].Books.Add(books[1]); authors[1].Books.Add(books[2]); ctx.Books.Add(books[0]); ctx.Authors.Add(authors[1]); await ctx.SaveChangesAsync(); } }
插入后各表的数据如下: