Relational Database 中,实现多对多 (many to many) 主要靠额外添加一个记录对应关系的表格实现。
上边的模型里,一个学生可能同时在修很多门课,一门课里同时会有很多学生,是典型的多对多关系。这时候要靠「学生 (Student)」和「课程 (Course)」两个表格来记录哪个学生在哪门课中,就要额外再添加一个表格。也就是上边例子里的「注册 (Enrollment)」。
这时候如果学生 A 注册了 1, 2 这两门课程,学生 B 注册了 2, 3 这两门课程,记录这种多对多关系的「注册 (Enrollment)」表格中就有四条记录:
Enrollment | |
---|---|
Student | Course |
A | 1 |
A | 2 |
B | 2 |
B | 3 |
这个表格把一个复杂的「多对多」关系变成了两个简单点的「一对多」关系。 比如学生 A 的 Courses 属性 (1),对应的就是 Enrollment 中的 A1 和 A2 这两行 (多)。
Entity Framework
在 Entity Framework 中。建立 many to many 关系非常简单,可以通过以下三种方式。这里仅考虑 Code First 方法,实际上在新版本 EF Core 中,Designer 功能甚至被拿掉了, Code First 是首选的使用方式:
1. 不明确添加 FK,交给 EF
public class CollegeDb : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
}
public class Student
{
public int Id { get; set; }
public string LastName { get; set; }
public string GivenName {get; set;}
//多门课程
public ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
//多个学生
public ICollection<Student> Students { get; set; }
}
只需要在 CollegeDB
中将学生和课程添加为 DbSet
类型的属性,EF 就会自动判断出他们之间多对多的关系,并建立第三个表格。
这里不管是 Student
还是 Course
都没有添加 Foreign Key。但 EF 还是需要记录他们之间的关系的,所以它会自动生成 FK 给它们 (一般命名为 Course_Id
和 StudentId
)」。
2. 指定 FK
明确指定 FK 的方法是在 Student
类里多加一个 CourseId
属性;同样,在 Course
类里,可以多加一个 StudentId
属性。这样,就不再是像前边一样依赖 EF 自己添加的 xx_Id
。具体来说,当 EF 看到 Student
类中有 x
属性 (如 Courses
),它就会自动认定 xId
(如 CourseId
) 属性为 FK。对了, EF 中类似自动匹配都不区分大小写。
指定 FK 是一个良好的习惯,具体原因回头另写一篇吧。
这里说下用 annotation 和 Fluent API 进一步明确和细化 EF 映射产生数据库的规则。
Data Annotation 修饰
public class CollegeDb : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
}
public class Student
{
public int Id { get; set; }
public string LastName { get; set; }
public string GivenName {get; set;}
//多个课程
public int CourseId { get; set; }
[Foreign Key("CourseId")] //非必需
public ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
//多个学生
public int StudentId { get; set; }
[Foreign Key("StudentId")] //非必需
public ICollection<Student> Students { get; set; }
}
可以看到这段代码里多了 [Foreign Key("...")]
这样的 annotation,添加在某些属性之前。Data Annotation 就是通过在属性上边的添加说明,让最终数据库的样子比 EF 自动生成的更符合自己要求,如指定 Key、自定义 Table/Column 名字、限定某个 Column 里数据长度等等。具体到多对多关系实现上,用到的 annotation 是 [Foreign Key]
。顾名思义,它就是标记哪个属性是 FK 的。
比如
public class Student
{
...
public int CourseId { get; set; }
[Foreign Key("CourseId")]
public ICollection<Course> Courses { get; set; }
}
的意思就是:Student
类中的 Courses
属性,也就是指向 Course
表格的关系,实际指向的是 Course
中的 Id
一栏,在 Student
这里就记作 CourseId
。
既然前边说,EF 可以自动识别 CourseId
为 FK,那为什么有的时候还要添加 [Foreign Key("CourseId")]
这样的 annotation 来再次声明?这是因为有的时候 EF 会过于死板。
比如
public class KiwiProfessor
{
...
public int PaperId { get; set; }
//[Foreign Key("PaperId")]
public ICollection<Course> Papers { get; set; }
}
KiwiProfessor
们喜欢称呼一门课为一个 Paper,而不是 Course。于是他们的属性中有 Papers
(当然使用的还是 Course
类),同时添加了 PaperId
作为 FK。然而在实际使用中,EF 并不会将 PaperId
当成 FK,而是会严格使用 Course
另外建立
一个 Paper_CourseId
作为 FK,多此一举。
这时候使用注释中的 annotation 就能制定直接使用 PaperId
作为 FK。
指定 FK + Fluent API
Fluent API 与 Data Annotation 相比更加灵活强大,它可以在某些情况下更好的隐藏 model,可以指定数据库删除数据时如何处理另一个表格上关联的数据等等……
也有人认为 Fluent API 更加干净,因为它是直接在程序 DbContext 的类里写的规则,指定 DbContext 所对应的数据库的 schema。这样一来, Student、Course 这些类就只是 POCO 了。
public class CollegeDb : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
//这里是关键
protected override void OnModelCreating(ModelBuilder modelBuilder){
modelBuilder.Entity<Student>().HasMany(s => s.Courses).WithMany();
}
}
//下边与前边代码一样,也可以与 Data Annotation 混用
public class Student
{
...
}
public class Course
{
...
}
Fluent API 中的 lambda 表达式很重要,它指明了哪个属性是 navigation property,尤其是在下边情况中:
假如研究生们还有一个属性 CoursesTutoring
,记录他们担任助教的课程 (一门课只有一个助教)。
public class GraduateStudent
{
...
public ICollection<Course> CoursesEnrolled { get; set; } //同时上多门课
public ICollection<Course> CoursesTutoring { get; set; } //同时担任多门课的助教
}
学生与课程之间就有了两个关系: 一个多对多关系,一个一对多关系。
这个时候, Fluent API 里边 modelBuilder.Entity<Student>().HasMany(s => s.CoursesEnrolled).WithMany();
里边的 HasMany(s => s.CoursesEnrolled)
就很有必要了,他指明了这个多对多关系是在说 CoursesEnrolled
。
Entity Framework Core
EF Core 截止到今天都还不支持自动建立多对多关系。而是必须通过手动的添加第三个表格 (join table) 来实现。
Join table 中应该有四个属性,其中两个是关系两头的 PK,共同组成 join table 的 PK。同时,这两个 PK 又分别是其中一个关系的 FK。 另外两个属性,就是 navigation property 了:
public class StudentCourse{
public int StudentId { get; set; } //PK, FK
public int CourseId { get; set; } //PK, FK
public Student Student { get; set; } // navigation property
public Course Course { get; set; } // navigation property
}
相应的,在 Student
和 Course
中的属性都要调整一下,navigation property 都不再是直接指向对方,而是指向中间的 StudentCourse
。否则 EF Core 会懵圈:
public class Student
{
public int Id { get; set; }
public string LastName { get; set; }
public string GivenName {get; set;}
public ICollection<StudentCourse> StudentCourses { get; set; } //指向 join table
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public ICollection<StudentCourse> StudentCourses { get; set; } //指向 join table
}
最后一步,就是使用 Fluent API 明确我们手动添加的 StudentCourse
和 Student
、Course
之间的关系:
public class CollegeDb : DbContext
{
public CollegeDb(DbContextOptions options) : base(options)
{
//constructor, 在 EF Core 中最好加上。但里边不需要有任何内容,直接继承 base(options) 即可
}
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<StudentCourse> StudentCourses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentCourse>().HasKey(sc => new { sc.StudentId, sc.CourseId }); // 指明用 StudentId 和 CourseId 做联合 PK
//建立与 Student 间的一对多关系
modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.Student)
.WithMany(s => s.StudentCourses)
.HasForeignKey(sc => sc.StudentId);
//建立与 Course 间的一对多关系
modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.Course)
.WithMany(c => c.StudentCourses)
.HasForeignKey(sc => sc.CourseId);
}
}
由于 EF Core 似乎还不支持通过 Data Annotation 指明联合 PK (composite primary key)。所以这里只能采用 Fluent API。
同时 EF Core 中最好给 DbContext 加一个以 DbContextOptions
为参数的 constructor。如果不加的话,EF Core 有时会搞不清楚你的 Server Provider 等信息在 build 时报错。