自引用关系

在一对多关系中,还有一种特殊的情况,就是 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();

    }

    
}

插入后各表的数据如下: