EF Core 关系配置

在进行项目开发的时候,很少有一张数据库表是单独存在的,因为大部分数据库表之间都有关系,这也正是”关系数据库”的价值所在。

作为一个ORM框架,EF Core不仅能帮助开发人员简化单张表的处理,在处理表之间的关系上也非常强大。

EF Core支持一对多,多对多,一对一等关系。

1. 一对多

一对多是常见的实体类间的关系,比如文章和评论的关系就是一对多的关系,也就是一篇文章对应多条评论。

下面通过文章和评论这两个实体类来讲解一对多关系的配置。

首先定义文章的实体类Article和评论的实体类Comment

C#
  public class Article
  {
      /// <summary>
      /// 主键
      /// </summary>
      public long Id { get; set; }
      /// <summary>
      /// 标题
      /// </summary>
      public string? Title { get; set; }
      /// <summary>
      /// 内容
      /// </summary>
      public string? Content { get; set; }
      /// <summary>
      /// 此文章的多条评论
      /// </summary>
      public List<Comment>? Comments { get; set; }

  }

  public class Comment
  {
      /// <summary>
      /// 主键
      /// </summary>
      public long Id { get; set; }
      /// <summary>
      /// 评论属于哪篇文章
      /// </summary>
      public Article? Article { get; set; }
      /// <summary>
      /// 评论内容
      /// </summary>
      public string? Message { get; set; }

  }

在上面的实体类中,我们看到文章的实体类Article中定义了一个Comment类型的List属性,因为一篇文章可能有多条评论;

评论的实体类Comment中定义了一个Article类型的属性,因为一条评论只能属于一篇文章。

EF Core中实体类之间关系的配置采用如下的模式:HasXXX(…).WithYYY(…)。

我们知道,Has在英语中是”有”的意思,With在英语中是”带有”的意思,因此HasXXX(…).WithYYY(…)就代表”A实体类对象有XXX个B实体类对象,B实体类对象带有YYY个A实体类对象”。

其中HaxXXX(…)用于设置当前这个实体类和关联的另一个实体类的关系,WithYYY(…)用来反向配置实体类的关系。

XXX、YYY有One和Many这两个可选值。

我们在A实体类中配置builder.HasOne<B>(…).WithMany(…)就表示A和B是”一对多”的关系,也就是一个A实体类的对象对应一个B实体类的对象,而一个B实体类的对象有多个A实体类的对象与之对应;

如果A实体类中配置builder.HasOne<B>(…).WithOne(…)就表示A和B是”一对一”的关系,也就是一个A实体类的对象对应一个B实体类的对象,而一个B实体类的对象也有一个A实体类的对象与之对应;

如果在A实体类中配置builder.HasMany<B>(…).WithMany(…)就表示A和B是”多对多”的关系,也就是一个A实体类的对象对应多个B实体类的对象,而一个B实体类的对象也有多个A实体类的对象与之对应。

根据上面所讲,我们对两个实体类使用FluentAPI进行关系的配置

C#
 public class ArticleConfig : IEntityTypeConfiguration<Article>
 {
     public void Configure(EntityTypeBuilder<Article> builder)
     {
         builder.ToTable("T_Articles");
         builder.Property(p=>p.Content).IsRequired().IsUnicode();
         builder.Property(a=>a.Title).IsRequired().IsUnicode().HasMaxLength(255);
     }
 }

 public class CommentConfig : IEntityTypeConfiguration<Comment>
 {
     public void Configure(EntityTypeBuilder<Comment> builder)
     {
         builder.ToTable("T_Comments");
         builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments).IsRequired();
         builder.Property(c => c.Message).IsRequired().IsUnicode();
     }
 }

对于一对多的关系的配置,主要就在对Comment实体类配置的第16行代码中;

C#
builder.HasOne<Article>.WithMany(a=>a.Comments).IsRequired();

因为这个关系的配置写在Comment实体类的配置中,所以这行代码的意思就是”一条评论对应一篇文章,一篇文章多条评论”。

HasOne<Article>(c=>c.Article)中的Lambda表达式c=>c.Article表示Comments类的Article属性是指向Article实体类型的,因为存在一个实体类中有多个属性为同一个实体类型的可能性。

比如”请假单”实体类中有申请者、主管、考勤专员等多个属性为”用户”实体类型,因此我们需要在HasOne的参数中指定配置的是哪个属性。

需要注意的是,HasOne方法有多个重装,调用的时候不要调用错了,初学者容易犯错的地方就是调用无参数的HasOne方法,写成builder.HasOne<Article>().WithMany(a=>a.Comments),这样就会造成运行结果和我们讲解的结果不一致的情况。

WithMany(a=>a.Comments)表示一个Article对应多个Comment,并且在Article中可以通过Comments属性访问到相关的Comments对象。

很显然,HasXXX(…)参数中的Lambda表达式配置的是当前这个类的属性,而WithXXX(…)参数中的Lambda表达式配置的是关系另一端的实体类的属性。

IsRequired表示Comment中的Article属性是不可以为空的,如果项目启用了”可空引用类型”,这里也可以不配置IsRequired,因为Artcile属性是不可为空的Article类型。

上面的代码编写完成后,执行EF Core迁移,随后在数据库中就可以看到生成的两张表。

其中T_Comments表的ArticleId列是一个指向T_Articles表Id列的外键。

接下来,我们编写代码测试数据的插入。

C#
  Article a1 = new();
  a1.Title = "肯德基疯狂星期四";
  a1.Content = "V我50";
  Comment c1 = new() { Message = "微信收款到账50元" };
  Comment c2 = new() { Message = "Wechat收款500元"};
  Comment c3 = new() { Message = "支持!" };
  a1.Comments.Add(c1);
  a1.Comments.Add(c2);
  a1.Comments.Add(c3);
  using BookDbContext db = new ();
  db.Articles.Add(a1);
  await db.SaveChangesAsync();

上面的代码执行完成后,我们再查询一下数据库,会发现数据已经插入成功了。

可以看到,只要把创建的Comment类的对象添加到Article对象的Comments属性的List中,然后把Article对象添加到db.Articles中,就可以把相关联的Comment对象添加到数据库中,不需要显式为Comment对象的Article属性赋值,也不需要显示地把新创建的Comment类型的对象添加到上下文中,因为我们的关系配置可以让EF Core自动完成这些工作。

2. 关联数据的获取

EF Core的关系配置不仅能帮我们简化数据的插入,也可以简化关联数据的获取。

如下面代码所示,把ID ==12的文章及其评论输出

C#
  using BookDbContext bdc = new();
  Article a = bdc.Articles.Include(a => a.Comments).Single(a => a.Id == 12);
  await Console.Out.WriteLineAsync(a.Title);
  foreach (var item in a.Comments!)
  {
      await Console.Out.WriteLineAsync($"{item.Id}{item.Message}");
  }

注意,Include方法是定义在Microsoft.EntityFrameworkCore命名空间中的扩展方法,因此在使用这个方法之前,需要在代码中添加对Microsoft.EntityFrameworkCore命名空间的引用。

运行结果如图:

接着我们查看一些上面代码生成的SQL语句:

可以看到,C#代码被翻译成了使用Left Join语句对T_Artucles和T_Comments表进行关联的查询的SQL语句。

起到关联查询作用的就是Include方法,它用来生成对其他关联实体类的查询操作。

如果我们把代码中的Include去掉

C#
  using BookDbContext bdc = new();
  Article a = bdc.Articles.Single(a => a.Id == 12);
  await Console.Out.WriteLineAsync(a.Title);
  foreach (var item in a.Comments!)
  {
      await Console.Out.WriteLineAsync($"{item.Id}{item.Message}");
  }

再来查看一下上面的代码对应的SQL语句

可以看到生成的SQL语句只是查询T_Articles表,没有使用Join语句关联查询T_Comments表,因此我们无法获取Comments属性中的数据。

在EF Core中,我们也可以在实体类中使用AutoInclude配置特定的导航属性自动进行Include操作,但是这容易被滥用而导致性能问题,因此不推荐使用它。

对AutoInclude感兴趣朋友们可以查看EF Core对应的文档。

EF Core默认使用Join操作进行关联对象数据的加载,在有些情况下Join的性能会比较低,如果我们先查询主表,然后执行一次查询关联对象的表来进行分布加载,也许能获得更好的性能。

从EF Core5.0开始,我们可以通过”拆分查询”实现在单独的查询中加载关联对象的数据,可以查看EF Core文档中AsSplitQuery部分的内容了解其用法。

3. 实体类对象的关联追踪

我们通过a1.Comments.Add(c1)把新创建的Comment对象插入Article对象的Comments属性中,这样a1和c1就建立起了关联关系。

由于EF Core中配置了两个实体类的关系,因此我们只要通过Comments属性把新创建的Comment对象和a1对象关联起来,EF Core就会自动保存Comments对象。

其实我们也可以不给Comments属性添加对象,而改为通过给Comment对象的Article属性赋值的方式完成数据的插入,如下所示

C#
  Article a1 = new();
  a1.Title = "我要准备睡觉了捏";
  a1.Content = "我要睡到下午五点";
  Comment c1 = new() { Message = "wdads",Article = a1 };

  Comment c2 = new() { Message = "周末就这样过", Article = a1 };

  using BookDbContext bdc = new();

  bdc.Comments.Add(c1);
  bdc.Comments.Add(c2);

  await bdc.SaveChangesAsync();

上述代码没有显式地把Article对象添加到bdc.Articles中,但是由于我们把新创建的Comment对象的Article属性设置为a1,因此我们也可以正常地在数据库中插入数据到T_Articles、T_Comments两张表中。

EF Core就是这样方便,只要我们的代码把实体类之间的关系设定了,EF Core就会顺着对象之间的关系完成数据的操作。

4. 关系的外键属性的设置

EF Core还支持其他对关系的设置。

比如对于关系设置OnDelete(DeleteBehavior.SetNull),当关联的对象对应的数据行从数据库删除之后,使用这行数据的关联表中对应列的值就会被设置为null,而不是把关联的数据删除。

不过现在很多项目中的数据删除都不是真的从数据库删除数据,而是通过把对应数据行的特定列的值更新为”已删除’的方式实现数据库的”软删除”,因此这里不对OnDelete做详细讲解。

如果需要真的删除数据,并且要定制级联删除行为的话,请查看官方文档。

EF Core会根据命名规则为”一对多的”多”端的实体类创建一个外键列。

比如上面的这个例子中,T_Comments表中就有一个自动创建的列ArticleId,在代码中我们不需要对这个列进行处理。

但是有的时候,我们可能只想获取这个外键列的值,在这种情况下,我们也需要进行关联查询。

比如使用以下代码把所有评论的Id、Message以及ArticleId输出。

C#
   using BookDbContext bdc = new();

   foreach (var item in bdc.Comments.Include(c=>c.Article))
   {
       await Console.Out.WriteLineAsync($"{item.Id}{item.Message}{item.Article?.Id}");
   }

上面的代码把T_Comments表和T_Articles表使用Join进行关联查询才能获取ArticleId列的值。

但是其实T_Comments表中是有ArticleId列的,我们完全不需要关联T_Articles表进行查询,因为这样做会消耗数据库服务器的资源。

如果有单独获取外键列的值的需要,我们可以在实体类中显式声明一个外键属性。

比如,在Comment类中增加一个long类型的ArticleId属性,然后在关系配置中通过HasForeignKey(c=>c.ArticleId)指定这个属性为外键即可。

增加外键配置
C#
  public class Comment
  {
      /// <summary>
      /// 主键
      /// </summary>
      public long Id { get; set; }
      /// <summary>
      /// 外键
      /// </summary>
      public long ArticleId { get; set; }
      /// <summary>
      /// 评论属于哪篇文章
      /// </summary>
      public Article? Article { get; set; }
      /// <summary>
      /// 评论内容
      /// </summary>
      public string? Message { get; set; }

  }
  
    public class CommentConfig : IEntityTypeConfiguration<Comment>
  {
      public void Configure(EntityTypeBuilder<Comment> builder)
      {
          builder.ToTable("T_Comments");
          builder.HasOne<Article>(c => c.Article)
              .WithMany(a => a.Comments)
              .IsRequired()
              .HasForeignKey(c=>c.ArticleId);
          builder.Property(c => c.Message).IsRequired().IsUnicode();
      }
  }

这样我们就可以使用如下代码把所有评论的Id、Message以及ArticleId输出。

C#
    using BookDbContext bdc = new();

    foreach (var item in bdc.Comments)
    {
        await Console.Out.WriteLineAsync($"{item.Id}{item.Message}{item.Article?.Id}");
    }

上面的代码中没有使用Include关联Article实体类进行查询,因此生成的SQL语句只通过查询T_Comments表就可以获取需要的数据,性能会好很多。

当然,采用这种方式我们需要再额外维护一个外键属性,因此一般情况下我们不需要这样声明。

毕竟简洁就是美。

5. 单向导航属性

之前的关系配置的例子中,在Article实体类中声明了Comments属性指向Comment类,这样我们不仅可以通过Comment类的Article属性获取评论对应的文章信息,还可以通过Article类的Comments属性获取文章的所有评论信息。

这样的关系叫作”双向导航”。

双向导航让我们可以通过任何一方的对象获取对方的信息,但是有时候我们不方便声明双向导航。

比如在大部分系统中,基础的”用户”实体类会被非常多的其他实体类引用。

举例:

“请假单”中会有”申请者””审批者”等”用户”实体类型的属性,”报销单”中会有”创建者””责任财务人员””主管”等”用户”实体类型的属性。

因此系统中会有几十个甚至上百个实体类都有”用户”实体类型的属性,但是”用户”实体类不需要位每个实体类都声明一个导航属性。

这种情况下,我们就需要一种只在”多端”声明导航属性,而不需要在”一端”声明导航的属性的单向导航机制。

这种单向导航属性的配置其实很简单,只要在WithMany方法中不指定属性即可。

下面以”用户””请假申请”两个实体类举例。

首先,创建”用户”实体类User

C#
 public class User()
 {
     public long Id { get; set; }
     /// <summary>
     /// 姓名
     /// </summary>
     public string? Name { get; set; }
 }

可以看到,User实体类中没有声明指向”请假申请”等实体类的属性。

接下来,再创建”请假申请”实体类Leave

C#
  public class Leave
  {
      public long Id { get; set; }
      /// <summary>
      /// 申请者
      /// </summary>
       public User Requester { get; set; } = new();
      /// <summary>
      /// 审批者
      /// </summary>
      public User? Apperover { get; set; }
      /// <summary>
      /// 说明
      /// </summary>
      public string? Remarks { get; set; }
      /// <summary>
      /// 开始日期
      /// </summary>
      public DateTime From { get; set; }
      /// <summary>
      /// 结束日期
      /// </summary>
      public DateTime To { get; set; }
      /// <summary>
      /// 状态
      /// </summary>
      public int Status { get; set; }

  }

可以看到,Leave类中有Requester、Approver两个User类型的属性,它们都是单向导航属性。

一个请假单中一定存在申请者,因此Requester属性不可为空;

还未审批的请假单是不存在审批者的,因此Approver属性为可空类型。

然后我们对这两个实体类进行配置。

C#
public DbSet<User> Users { get; set; }

public DbSet<Leave> Leaves { get; set; }


 class UserConfig : IEntityTypeConfiguration<User>
 {
     public void Configure(EntityTypeBuilder<User> builder)
     {
        builder.ToTable("T_Users");
        builder.Property(u=>u.Name)
             .IsRequired().HasMaxLength(100).IsUnicode();
     }
 }
 
 class LeaveConfig : IEntityTypeConfiguration<Leave>
 {
     public void Configure(EntityTypeBuilder<Leave> builder)
     {
         builder.ToTable("T_Leaves");
         builder.HasOne<User>(ls => ls.Requester).WithMany();
         builder.HasOne<User>(ls=>ls.Apperover).WithMany();
         builder.Property(ls=>ls.Remarks)
                .HasMaxLength(1000).IsUnicode();
     }
 }

可以看到,Requester、Approver这两个属性都是单向导航属性,因为WithMany方法中没有传递参数,当然也没有合适的参数给WithMany方法,毕竟User类中没有指向Leave类的属性。

插入数据
C#
 User u1 = new() { Name = "ICHI" };
 Leave l1 = new() 
 {
     Requester = u1,
     From = new (2024,4,28),
     To = new(2024,4,29),
     Remarks = "生病了,请假去玩",
     Status = 0
 };
 using BookDbContext bdc = new();

 bdc.Users.Add(u1);
 bdc.Leaves.Add(l1);

 await bdc.SaveChangesAsync();

由于User实体类中没有指向Leave实体类的属性,如果要实现”获取一个用户的所有请假申请”,我们可以直接到请假申请实体类的DbSet中去查询。

如下代码所示,获取名字为ICHI的所有请假申请

C#
   using BookDbContext bdc = new();
   User u = await bdc.Users.SingleAsync(c=>c.Name!.Equals("ICHI"));
   foreach (var item in bdc.Leaves.Where(l=>l.Requester == u))
   {
       await Console.Out.WriteLineAsync(item.Remarks);
   }

在实际项目开发中,对于主从结构的”一对多”表关系,我们一般是声明双向导航属性;

对于其他的”一对多”表关系,我们则需要根据情况决定是使用单向导航属性还是使用双向导航属性,比如被很多表都引用的基础表,一般都是声明单向导航属性。

6. 关系配置在哪个实体类中

实体类之间的关系是双向的,以前面讲到的实体类为例,我们可以说Article和Comment之间的关系是”一对多”,也就是一个Article对应多个Comment;

也可以说Comment和Article之间的关系是”多对一”,也就是多个Comment对应一个Article。

站在不同的角度,就有不同的说法,但是本质上它们指的是同一个东西。

因此,两张表之间的关系可以配置在任何一端。比如上面的Article和Comment这两个实体类,我们把它们的关系配置交换。

以下是修改了的新的配置

C#
  public class ArticleConfig : IEntityTypeConfiguration<Article>
  {
      public void Configure(EntityTypeBuilder<Article> builder)
      {
          builder.ToTable("T_Articles");
          #region 新配置
          builder.HasMany<Comment>(a => a.Comments).WithOne(c => c.Article);
          #endregion

          builder.Property(p=>p.Content).IsRequired().IsUnicode();
          builder.Property(a=>a.Title).IsRequired().IsUnicode().HasMaxLength(255);
      }
  }

  public class CommentConfig : IEntityTypeConfiguration<Comment>
  {
      public void Configure(EntityTypeBuilder<Comment> builder)
      {
          builder.ToTable("T_Comments");
          //builder.HasOne<Article>(c => c.Article)
          //    .WithMany(a => a.Comments)
          //    .IsRequired()
          //    .HasForeignKey(c=>c.ArticleId);
          builder.Property(c => c.Message).IsRequired().IsUnicode();

      }
  }

可以看到,我们把关系的配置从CommentConfig类中移动到了ArticleConfig类中。

当然,由于配置的位置变了,

我们把CommentConfig类中的HasOne<Article>(c=>c.Article).WithMany(a=>a.Comments)

改成了ArticleConfig类中的HasMany<Comment>(a=>a.Comments).WithOne(c=>c.Article)。

执行数据库迁移后,重新执行代码,我们会发现数据库结构和之前的没有任何区别,也就是说这两种配置方式的效果是一样的。

当然,对于单向导航属性,我们只能把关系配置到一方。

因此,考虑到有单向导航属性的可能,我们一般都用HasOne(…).WithMany(…)这样的方式进行配置。

7. 一对一

实体类之间还可以有一对一关系,比如”采购申请单”和”采购订单”就是一对一关系。

假设实现一个电商网站,那么”订单”和”快递信息”可以定义成两个实体类,这两个实体类之间就是一对一的关系:

一个订单对应一个快递信息,一个快递信息对应一个订单。

真实系统中的订单快递信息等实体类中的属性是非常复杂的。

我们这里侧重讲解EF Core技术,因此对于这些业务相关的需求做了简化。

首先,我们声明订单实体类Order和快递信息实体类Delivery

C#
 class Order
 {
     public long Id { get; set; }

     public string? Name { get; set; }

     public string? Address { get; set; }

     public Delivery? Delivery { get; set; }
 }

 class Delivery
 {
     public long Id { get; set; }
     public string? CompanyName { get; set; }

     public string? Number { get; set; }

     public Order? Order { get; set; }

     public long OrderId { get; set; }
 }

可以看到,Order和Delivery类中都分别声明了一个指向对象的属性,这样就构成了一对一的关系。

在一对多的关系中,我们需要在”多”端有一个指向”一”端的列,因此除非我们需要显示地声明一个外键属性,否则EF Core会自动在多端的表中生成一个指向一端的外键列,不需要我们显示地声明外键属性。

但是对于一对一关系,由于双方是”平等”的关系,外键列可以建在任意一方,因此我们必须显式地在其中一个实体类中声明一个外键属性。

就像上面实体类定义中,Delivery类中声明了一个外键属性OrderId,当然我们也可以改成在Order类中声明一个外键属性DeliveryId,效果是一样的。

接下来我们对这两个实体类进行配置。

C#
 class OrderConfig : IEntityTypeConfiguration<Order>
 {
     public void Configure(EntityTypeBuilder<Order> builder)
     {
         builder.ToTable("T_Order");
         builder.Property(o => o.Address).IsUnicode();
         builder.Property(o=>o.Name).IsUnicode();
         builder.HasOne<Delivery>(l => l.Delivery)
                .WithOne(d=>d.Order)
                .HasForeignKey<Delivery>(d=>d.OrderId);

     }
 }
 class DeliveryConfig : IEntityTypeConfiguration<Delivery>
 {
     public void Configure(EntityTypeBuilder<Delivery> builder)
     {
         builder.ToTable("T_Delivery");
         builder.Property(d=>d.CompanyName).IsUnicode().HasMaxLength(10);
         builder.Property(d=>d.Number).HasMaxLength(50);
     }
 }

和一对多关系类似,在一对一关系中,把关系放到哪一方的实体类的配置中都可以。

这里把关系的配置放到了Order类的配置中。

这里的配置同样遵守HasXXX(…).WithYYY(…)的模式,由于双方都是一端,因此使用HasOne(…).WithOne(…)进行配置。

由于在一对一关系中,必须显示地指定外键配置在哪个实体类中,因此我们通过HasForeignKey方法声明外键对应的属性。

然后,我们编写代码测试一下数据的插入以及查询。

C#
 using BookDbContext bookDbContext = new BookDbContext();

 Order order = new Order();

  order.Address = "深圳市";
  order.Name = "恰恰瓜子";
  Delivery delivery = new Delivery();
  delivery.CompanyName = "德邦快递";
  delivery.Number = "SN87655654";
  delivery.Order = order;
  bookDbContext.deliveries.Add(delivery);
  await bookDbContext.SaveChangesAsync();
  Order o1 = await bookDbContext.orders
      .Include(o => o.Delivery)
      .FirstAsync(o => o.Name!.Contains("瓜子"));

  await Console.Out.WriteLineAsync($"名称:{o1.Name},单号:{o1.Delivery?.Number}");
8. 多对多

多对多是比较复杂的一种实体类间的关系。

在EF Core的旧版本中,我们只能通过两个一对多关系模拟实现多对多关系。

从EF Core5.0开始,EF Core提供了对多对多关系的支持。

多对多指的是A实体类的一个对象可以被多个B实体类的对象引用,B实体类的一个对象也可以被多个A实体类的对象引用。

比如在学校里,一个老师对应多个学生,一个学生也有多个老师,因此老师和学生之间的关系就是多对多。

下面我们就使用”学生-老师”这个例子实现多对多关系。

首先,我们声明学生类Student和老师类Teacher两个实体类

C#
 public class Student
 {
     public long Id { get; set; }
     public string? Name { get; set; }

     public List<Teacher>? Teachers { get; set; }

 }
 public class Teacher
 {
     public long Id { get; set; }
     public string? Name { get; set; }
     public List<Student>? Students { get; set; }
 }

可以看到,学生类Student中有一个List类型的Teachers代表这个学生的所有老师,同样地,老师类Teacher中也有一个List类型的Student代表这个老师的所有学生。

接下来,我们开始对学生和老师实体类进行配置

C#
  class TeacherConfig : IEntityTypeConfiguration<Teacher>
  {
      public void Configure(EntityTypeBuilder<Teacher> builder)
      {
          builder.ToTable("T_Teachers");
          builder.Property(s => s.Name).IsUnicode().HasMaxLength(20);
      }

  }

  class StudentConfig : IEntityTypeConfiguration<Student>
  {
      public void Configure(EntityTypeBuilder<Student> builder)
      {
          builder.ToTable("T_Students");
          builder.Property(s => s.Name).IsUnicode().HasMaxLength(20);
          builder.HasMany<Teacher>(s => s.Teachers)
                 .WithMany(t => t.Students)
                 .UsingEntity(j => j.ToTable("T_Students_Teachers"));
      }
  }

同样地,多对多的关系配置可以放到任何一方的配置类中,这里把关系配置代码放到了Student类的配置中。

这里把关系配置代码放到了Student类的配置中。

这里同样采用的是HasXXX(…).WithYYY(…)的模式,由于是多对多,关系的两端都是”多”,因此关系配置使用的是HasXXX(…).WithMany(…)。

一对多和一对一都只要在表中增加外键列即可,但是在多对多关系中,我们必须引入一张额外的数据库保存两种表之间的对应关系。

在EF Core中,使用UsingEntity(j=>j.ToTable(“T_Students_Teachers”))的方式配置中间表。

对上面的代码执行迁移,可以发现数据库中增加了三张数据库表。

可以看到,数据库有一张额外的关系表T_Students_Teacjers,这张表中有指向T_Student表的外键列StudentsId,也有指向T_Teachers表的外键列TeachersId。

T_Students_Teachers表保存了T_Student表和T_TeachersId表中数据之间的对应关系,而我们不需要为这张关系表声明实体类。

接下来,我们在上下文中增加Teacher、Students对应的DbSet属性,然后执行代码完成数据的插入。

C#
  Student s1 = new() { Name = "lily" };
  Student s2 = new() { Name = "tom" };
  Student s3 = new() { Name = "bob" };

  Teacher t1 = new() { Name = "老张", Students = [s1, s2] };
  Teacher t2 = new() { Name = "老李", Students = [s2, s3] };
  Teacher t3 = new() { Name = "老六", Students = [s3, s1] };



  using BookDbContext TDC = new();

  await TDC.AddRangeAsync(s1, s2, s3);
  await TDC.AddRangeAsync(t1, t2, t3);
  await TDC.SaveChangesAsync();

在第13、14行代码中,通过AddRangeAsync方法把多个对象批量加入上下文中。

需要注意的是,AddRangeAsync只是循环调用Add把多个实体类加入上下文,是对Add方法的简化调用,在使用SaveChangesAsync的时候,这些实体类仍然是被逐条地插入数据库中的。

代码执行完成后,查看三张数据库表中的数据

我们再查询一下所有的老师,并且列出他们的学生

C#
 using BookDbContext TDC = new();

 foreach (var item in TDC.Teachers.Include(t=>t.Students)) 
 {
     await Console.Out.WriteLineAsync($"老师姓名{item.Name}");
     foreach (var s in item.Students!)
     {
         await Console.Out.WriteLineAsync($"学生姓名{s.Name}");
     }
 }
9. 基于关系的复杂查询

基于EF Core的实体类之间的关系配置,不仅可以让数据的插入、查询操作变得非常方便,而且可以让基于关系的过滤条件的实现也变得非常简单。

使用Article和Comments实体类查询评论中含有”微信”的文章。

C#
 using BookDbContext TDC = new();

 var articles = 
     TDC.Articles.Where(a=>a.Comments!
                 .Any(c=>c.Message!.Contains("微信")));
foreach (var article in articles)
 {
     await Console.Out.WriteLineAsync($"{article.Id},{article.Title}");
 }

在Where方法中,使用Any方法判断是否存在至少一条评论中包含”微信”的文章。

上面的代码生成的SQL语句如下:

SQL
  SELECT [t].[Id], [t].[Content], [t].[Title]
  FROM [T_Articles] AS [t]
  WHERE EXISTS (
  SELECT 1
  FROM [T_Comments] AS [t0]
  WHERE [t].[Id] = [t0].[ArticleId] AND [t0].[Message] LIKE N'%微信%')

可以看到,EF Core使用Exists加子查询实现C#代码,相比复杂的SQL语句,编写C#代码可以更加轻松地实现复杂的查询逻辑。

当然,EF Core可以帮我们完成很多事情,但不代表我们就不用关注底层生成的SQL语句。

比如上面编写的C#代码被EF Core翻译成了Exists加子查询的SQL语句。

根据数据库的不同以及数据特点的不同,上面的写法也许并不是性能最优的写法改写成以下代码也许性能更优。

SQL
 SELECT DISTINCT [t0].[Id], [t0].[Content], [t0].[Title]
 FROM [T_Comments] AS [t]
 INNER JOIN [T_Articles] AS [t0] ON [t].[ArticleId] = [t0].[Id]
 WHERE [t].[Message] LIKE N'%微信%'

可以看到,同样效果的代码被翻译成了使用Join查询实现的数据筛选。

根据具体情况不同,这种做法也许性能更好。

当然,对性能问题必须具体问题具体分析,没有一个确定性的结论。

总之,使用关系操作,在EF Core中进行跨表数据查询变得非常容易,但是开发人员仍然需要关注和调整EF Core底层生成的SQL语句,确保在系统的重要环节不会有明显的性能瓶颈。

订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
滚动至顶部