前面章节我们介绍了EFCore中单表的模型定义和增删改查,这篇笔记我们继续学习如何定义多表之间的关联关系。为了简单起见,本篇笔记我们定义数据模型的方式都是使用FluentAPI,关于如何使用数据注解定义关联模型可以参考微软官方文档。
一对多是最常见的关联关系,我们这里通过例子介绍如何定义一对多关联关系。下面例子中我们定义了数据模型ClassRoom
和Student
,它们具有一对多关系,一个教室关联多个学生。
ClassRoom.cs
namespace Gacfox.Demo.DemoNetCore;
public class ClassRoom
{
public long ClassRoomId { get; set; }
public string ClassRoomName { get; set; }
public List<Student> Students { get; set; } = new List<Student>();
}
ClassRoomEntityConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class ClassRoomEntityConfig : IEntityTypeConfiguration<ClassRoom>
{
public void Configure(EntityTypeBuilder<ClassRoom> builder)
{
builder.ToTable("t_classroom");
builder.HasKey(e => e.ClassRoomId);
builder.Property(e => e.ClassRoomId)
.ValueGeneratedOnAdd()
.HasColumnName("classroom_id");
builder.Property(e => e.ClassRoomName)
.HasColumnName("classroom_name")
.IsRequired();
}
}
ClassRoom
模型中,我们定义了Students
属性表示该教室下的学生列表。
Student.cs
namespace Gacfox.Demo.DemoNetCore;
public class Student
{
public long StudentId { get; set; }
public string StudentName { get; set; }
public int Age { get; set; }
public DateTime CreateTime { get; set; }
public long ClassRoomId { get; set; }
public ClassRoom ClassRoom { get; set; }
}
StudentEntityConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class StudentEntityConfig : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("t_student");
builder.HasKey(e => e.StudentId);
builder.Property(e => e.StudentId)
.ValueGeneratedOnAdd()
.HasColumnName("student_id");
builder.Property(e => e.StudentName)
.HasColumnName("student_name")
.IsRequired();
builder.Property(e => e.Age)
.HasColumnName("age")
.IsRequired();
builder.Property(e => e.CreateTime)
.HasColumnName("create_time")
.IsRequired();
builder.Property(e => e.ClassRoomId)
.HasColumnName("classroom_id");
builder.HasOne(e => e.ClassRoom)
.WithMany(e => e.Students)
.HasForeignKey(e => e.ClassRoomId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Student
模型中,我们定义了ClassRoom
属性表示该学生归属的教室,此外我们还定义了ClassRoomId
属性表示该学生关联的教室ID外键,这里注意该属性是可以省略不写的,即使不写EFCore也会帮我们在数据库中自动创建外键字段,但我这里习惯将其明确写出,这样如果有“从学生查询教室ID”的需求就可以不必把教室实体类查询出来,此外明确写出外键字段也方便我们对外键字段自定义列名。
数据模型配置中,我们使用了一组.HasOne().HasMany()
的链式调用,这些就是EFCore中定义模型关联的API,代码中我们对HasOne()
方法指定了模型中的ClassRoom
属性,对HasMany()
方法指定了关联模型中的Students
属性,这样就实现了一对多关联关系。该处链式调用我们还使用了HasForeignKey()
方法指定了我们之前定义的外键字段,如果之前没有明确定义外键字段则不必在此指定,此外我们还调用了OnDelete()
方法指明了外键的删除级联操作。
定义一对多关系的额外补充:
Student
的模型映射配置中指定一对多关联关系,实际上我们也可以在ClassRoom
模型配置中指定关联关系,此时就应该使用.HasMany().WithOne()
链式调用,不过我们还是推荐在一对多中一的一方指定关联关系,这样更符合一般人的思维逻辑。ClassRoom
模型没有定义Students
属性,此时就是一对多单向关联,我们在StudentEntityConfig.cs
中进行模型配置时可以使用builder.HasOne(e => e.ClassRoom).WithMany()
指定,这里WithMany()
方法可以不传任何参数。Program.cs
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
public static void Main()
{
using (MyDbContext myDbContext = new MyDbContext())
{
ClassRoom room = new ClassRoom { ClassRoomName = "一年一班" };
Student s1 = new Student { StudentName = "汤姆", Age = 18, CreateTime = DateTime.Now };
Student s2 = new Student { StudentName = "杰瑞", Age = 18, CreateTime = DateTime.Now };
room.Students.Add(s1);
room.Students.Add(s2);
myDbContext.ClassRooms.Add(room);
myDbContext.Students.Add(s1);
myDbContext.Students.Add(s2);
myDbContext.SaveChanges();
}
}
}
上面代码演示了向一对多关联的两张表中插入数据的代码,我们创建了1个ClassRoom
对象,然后又创建了2个Student
对象并将其关联在一起。注意虽然模型中我们定义的是双向关联,但实际代码中我们插入数据时没必要双向指定,只要指定一个方向的关联关系,EFCore就知道我们要实现的操作是什么了。
代码的最后我们保存了数据,实际上myDbContext.Students.Add(s1)
和myDbContext.Students.Add(s2)
甚至也可以省略不写,因为EFCore能够自动解析关联关系,代码中我们已经设置了关联,因此EFCore可以自动保存被关联的Student
对象,但这里我还是习惯将其明确写出,这样代码可读性更好。
自关联是一种特殊的一对多关系,自关联最常见的地方是系统菜单,一个菜单可能包含若干子菜单,这显然是一种自关联关系。
Menu.cs
namespace Gacfox.Demo.DemoNetCore;
public class Menu
{
public long MenuId { get; set; }
public string MenuName { get; set; }
public long? ParentMenuId { get; set; }
public Menu ParentMenu { get; set; }
public List<Menu> ChildMenus { get; set; } = new List<Menu>();
}
MenuEntityConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
internal class MenuEntityConfig : IEntityTypeConfiguration<Menu>
{
public void Configure(EntityTypeBuilder<Menu> builder)
{
builder.ToTable("t_menu");
builder.HasKey(e => e.MenuId);
builder.Property(e => e.MenuId)
.ValueGeneratedOnAdd()
.HasColumnName("menu_id");
builder.Property(e => e.MenuName)
.IsRequired()
.HasColumnName("menu_name")
.HasMaxLength(20);
builder.Property(e => e.ParentMenuId)
.HasColumnName("parent_menu_id")
.IsRequired(false);
builder.HasOne(e => e.ParentMenu)
.WithMany(e => e.ChildMenus)
.HasForeignKey(e => e.ParentMenuId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Program.cs
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
public static void Main()
{
using (MyDbContext myDbContext = new MyDbContext())
{
Menu m1 = new Menu { MenuName = "用户管理" };
Menu m2 = new Menu { MenuName = "系统管理" };
Menu m11 = new Menu { MenuName = "新增用户" };
Menu m12 = new Menu { MenuName = "导入用户" };
Menu m21 = new Menu { MenuName = "数据字典" };
m11.ParentMenu = m1;
m12.ParentMenu = m1;
m21.ParentMenu = m2;
myDbContext.Menus.Add(m1);
myDbContext.Menus.Add(m2);
myDbContext.Menus.Add(m11);
myDbContext.Menus.Add(m12);
myDbContext.Menus.Add(m21);
myDbContext.SaveChanges();
}
}
}
我们可以看到,定义一对多自关联和普通的一对多没有太大区别,我们正常定义数据模型的关联关系即可,只不过一对多的两侧都是同一个实体类。不过这里注意由于我们的外键ParentMenuId
可能为空(对于根节点菜单来说),因此我们这里将其C#类型定义为long?
可空类型,并在配置类中指定了IsRequired(false)
。
一对一关联定义的例子如下。
IdCard.cs
namespace Gacfox.Demo.DemoNetCore;
public class IdCard
{
public long CardId { get; set; }
public string CardCode { get; set; }
public Person Person { get; set; }
}
IdCardEntityConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class IdCardEntityConfig : IEntityTypeConfiguration<IdCard>
{
public void Configure(EntityTypeBuilder<IdCard> builder)
{
builder.ToTable("t_idcard");
builder.HasKey(e => e.CardId);
builder.Property(e => e.CardId)
.ValueGeneratedOnAdd()
.HasColumnName("card_id");
builder.Property(e => e.CardCode)
.HasColumnName("card_code")
.IsRequired();
}
}
Person.cs
namespace Gacfox.Demo.DemoNetCore;
public class Person
{
public long PersonId { get; set; }
public string PersonName { get; set; }
public long CardId { get; set; }
public IdCard IdCard { get; set; }
}
PersonEntityConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class PersonEntityConfig : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.ToTable("t_person");
builder.HasKey(e => e.PersonId);
builder.Property(e => e.PersonId)
.ValueGeneratedOnAdd()
.HasColumnName("person_id");
builder.Property(e => e.PersonName)
.HasColumnName("person_name")
.IsRequired();
builder.Property(e => e.CardId)
.HasColumnName("card_id")
.IsRequired();
builder.HasOne(e => e.IdCard)
.WithOne(e => e.Person)
.HasForeignKey<Person>(e => e.CardId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Program.cs
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
public static void Main()
{
using (MyDbContext myDbContext = new MyDbContext())
{
IdCard idCard = new IdCard { CardCode = "123456" };
Person person = new Person { PersonName = "李雷" };
person.IdCard = idCard;
myDbContext.IdCards.Add(idCard);
myDbContext.People.Add(person);
myDbContext.SaveChanges();
}
}
}
我们可以看到其实一对一和一对多没有什么太大的区别,我们使用.HasOne().WithOne()
写法定义即可。不过这里要注意,之前一对多我们可以省略外键字段不写,EFCore会帮我们自动创建,但一对一关系中我们必须明确写出外键,这是因为一对一关系中外键字段可以在两张表任意一侧,EFCore不知道要将外键字段自动创建到哪一侧,因此我们必须手动指定。
多对多关系比较特殊的是我们需要使用一张中间表来表达关联关系,下面是一个例子。
namespace Gacfox.Demo.DemoNetCore;
public class Author
{
public long AuthorId { get; set; }
public string AuthorName { get; set; }
public List<Book> Books { get; set; } = new List<Book>();
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class AuthorEntityConfig : IEntityTypeConfiguration<Author>
{
public void Configure(EntityTypeBuilder<Author> builder)
{
builder.ToTable("t_author");
builder.HasKey(e => e.AuthorId);
builder.Property(e => e.AuthorId)
.HasColumnName("author_id");
builder.Property(e => e.AuthorName)
.HasColumnName("author_name")
.IsRequired();
}
}
namespace Gacfox.Demo.DemoNetCore;
public class Book
{
public long BookId { get; set; }
public string BookName { get; set; }
public List<Author> Authors { get; set; } = new List<Author>();
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Gacfox.Demo.DemoNetCore;
public class BookEntityConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("t_book");
builder.HasKey(e => e.BookId);
builder.Property(e => e.BookId)
.HasColumnName("book_id");
builder.Property(e => e.BookName)
.HasColumnName("book_name")
.IsRequired();
builder.HasMany(e => e.Authors)
.WithMany(e => e.Books)
.UsingEntity<Dictionary<string, object>>(
"t_author_book",
join => join
.HasOne<Author>()
.WithMany()
.HasForeignKey("author_id"),
join => join
.HasOne<Book>()
.WithMany()
.HasForeignKey("book_id"),
join => join.HasKey("author_id", "book_id")
);
}
}
代码中我们定义了数据模型Author
和Book
的双向多对多关系,其中关联关系我们定义在Book
这一侧,实际上我们可以定义在任意一侧。BookEntityConfig
类中,我们使用了.HasMany().WithMany()
的方式定义多对多关系,EFCore中我们可以不显式的定义关联表实体类,这里我们使用UsingEntity()
方法来对关联表进行配置,该方法有很多重载,我们使用的是最复杂的一个,我们这里对关联表的表名、字段名、主键都进行了配置,有关其他重载的使用我们可以参考官方文档。
namespace Gacfox.Demo.DemoNetCore;
public class Program
{
public static void Main()
{
using (MyDbContext myDbContext = new MyDbContext())
{
Author a1 = new Author { AuthorName = "赵日天" };
Author a2 = new Author { AuthorName = "王后雄" };
Book b1 = new Book { BookName = "一万个为什么" };
Book b2 = new Book { BookName = "母猪的产后护理" };
Book b3 = new Book { BookName = "MySQL从删库到跑路" };
a1.Books.Add(b1);
a1.Books.Add(b2);
a2.Books.Add(b2);
a2.Books.Add(b3);
myDbContext.Authors.Add(a1);
myDbContext.Authors.Add(a2);
myDbContext.Books.Add(b1);
myDbContext.Books.Add(b2);
myDbContext.Books.Add(b3);
myDbContext.SaveChanges();
}
}
}
上面代码中我们向多对多关联表中插入了数据,执行后我们可以看到t_author
、t_book
、t_author_book
3张表都插入了数据。