前言:
想起多对多的关联,笔者就会想起大学数据库课本上那个经典的学生选课表
学生可以选多门课,课也可以有多个学生选择
这显然无法在两张表上建立多对多的关系
必须有一张中间表
正文
在EFCore5.0以前,甚至是EntityFramework时代,配置多对多关系甚至需要一个中间类
然后把这个中间类映射成那一张中间表,
从而
把两个类的多对多的关系才分为两个双向一对多的关系,
这样就明显将事情变得更复杂了
如果不清楚的话,我在这里画一张关系图
但是我在查看微软的EFCore文档时发现了在5.0的更新中已经支持默认的多对多关系的配置了
详情可以查看该章节:
https://docs.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-5.0/whatsnew
一.默认约定的方式配置多对多
现在我们依旧以学生选课为例,让EFCore为我们配置多对多的关系
//只需要在多对多关系的实体中设置一个关联对象的集合即可自动配置多对多关联
class Course
{
public int id { get; set; }
public string course_name { get; set; }
public string teacher_name { get; set; }
public ICollection<StudentCopy> students { get; set; }
}
class StudentCopy
{
public int id { get; set; }
public string name { get; set; }
public int age { get; set; }
public ICollection<Course> courses { get; set; }
}
class MMDbContext:DbContext
{
public DbSet<Course> courses { get; set; }
public DbSet<StudentCopy> studentCopies { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var Connection = "server=.;Database=xxxxxx;uid=xxxxxxxx;pwd=xxxxxxxxxxxx";
optionsBuilder.UseSqlServer(Connection);
}
}
在NuGet控制台中输入
add-migration initialcreate -c MMDbContext
update-database -Context MMDbContext
来看一下自动生成的建表代码:
protected override void Up(MigrationBuilder migrationBuilder)
{
//创建course表
migrationBuilder.CreateTable(
name: "courses",
columns: table => new
{
//设置列的类型和属性
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
course_name = table.Column<string>(type: "nvarchar(max)", nullable: true),
teacher_name = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
//设置主键
table.PrimaryKey("PK_courses", x => x.id);
});
//创建studentCopies表
migrationBuilder.CreateTable(
name: "studentCopies",
columns: table => new
{
//设置列的类型和属性
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
name = table.Column<string>(type: "nvarchar(max)", nullable: true),
age = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
//设置主键
table.PrimaryKey("PK_studentCopies", x => x.id);
});
//设置中间表CourseStudentCopy
migrationBuilder.CreateTable(
name: "CourseStudentCopy",
columns: table => new
{
//中间表中只有两个属性,看来是设置的联合主键了
coursesid = table.Column<int>(type: "int", nullable: false),
studentsid = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
//将coursesid,studentsid设置为联合主键
table.PrimaryKey("PK_CourseStudentCopy", x => new { x.coursesid, x.studentsid }); //同时也将coursesid和studentsid设置外键分别参照
//于course表,和studentCopies表的主键
table.ForeignKey(
name: "FK_CourseStudentCopy_courses_coursesid",
column: x => x.coursesid,
principalTable: "courses",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CourseStudentCopy_studentCopies_studentsid",
column: x => x.studentsid,
principalTable: "studentCopies",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
//甚至给studentid属性建了个索引,为什么不是为courseid建索引,这个就不得而知了
migrationBuilder.CreateIndex(
name: "IX_CourseStudentCopy_studentsid",
table: "CourseStudentCopy",
column: "studentsid");
}
表结构图则如下
现在测试一下是否能成功的插入关联对象
static void Main(string[] args)
{
//创建数据上下文
MMDbContext context = new MMDbContext();
//实例化几个对象
StudentCopy student = new StudentCopy();
student.name = "羅翔";
student.age = 21;
StudentCopy student1 = new StudentCopy();
student1.name = "孫笑川";
student1.age = 99;
Course course = new Course();
course.course_name = "張三說刑法";
course.teacher_name = "張三";
Course course1 = new Course();
course1.course_name = "這把怎麼輸";
course1.teacher_name = "盧本偉";
Course course2 = new Course();
course2.course_name = "李四說民法";
course2.teacher_name = "李四";
//设置多对多的关联
//注意:这里我只使用了student对象来维护关联
//甚至没有在数据库上下文中去保存我的course对象,
student.courses.Add(course);
student.courses.Add(course1);
student1.courses.Add(course);
student1.courses.Add(course1);
student1.courses.Add(course2);
context.studentCopies.Add(student);
context.studentCopies.Add(student1);
context.SaveChanges();
}
代码成功的运行并没有报错,那到数据库查看一下
数据都正确的写入了,
可见,它自动设置保存方面的级联,像双向一对多/多对一一样默认就可以单边维护关系,这就比较方便了
(当然,级联设置可以根据需要在FluentAPI中设置相对应的方式,像删除级联就有多种方式,例如改null,删除孤儿节点,或者一起极限删除…,这得在它的文档里好好查一查了)
(PS:Hibernate及JPA默认双方都维护关系,会导致多发一条SQL语句,需要加上inverse=true的注释)
现在我们来查一查我们刚才写入的数据
static void Main(string[] args)
{
MMDbContext context = new MMDbContext();
//通過LINQ查詢到羅翔的相關信息
var luoxiang = context.studentCopies.Include(s=>s.courses)
.Where<StudentCopy>(s => s.name.Equals("羅翔"))
.First();
//Include是預想加載關聯屬性的命令,如果不加上 Include,默認為懶加載
//即Lazy模式,之後在查詢的文章中再研究一下
var Lcourse = luoxiang.courses;
foreach(Course c in Lcourse)
{
//打印一下剛才插入的表信息,
Console.WriteLine("羅翔選的課為:"+c.course_name+"講師為:"+c.teacher_name);
}
}
結果:
羅翔選的課為:張三說刑法講師為:張三
羅翔選的課為:這把怎麼輸講師為:盧本偉
二.老版本FluentAPI使用中间类配置多对多
public class StudentCourse
{
//使用确定的名称方式,EFCore会将其配置为外键参照Students表,
//当然,你也可以定义为Sid,StudentId这种约定的名字,也可以定义自己随便定义,不过要在FluentAPI中指明
//modelBuilder.Entity<StudentCourse>()
// .HasOne<Student>(sc => sc.Student)
// .WithMany(s => s.StudentCourses)
// .HasForeignKey(sc => sc.SId);
public int StudentId { get; set; }
public Student Student { get; set; }
//使用确定的名称方式,同上,EFCore会将其配置为外键参照Courses表
public int CourseId { get; set; }
public Course Course { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
public IList<StudentCourse> StudentCourses { get; set; }
}
public class Course
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public string Description { get; set; }
public IList<StudentCourse> StudentCourses { get; set; }
}
public class SchoolContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.xxxxx;Database=xxxx;uid=xxxx;pwd="xxxx");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentCourse>().HasKey(sc => new { sc.StudentId, sc.CourseId });
//如果中间类的主键属性不遵从约定,则需在下面再配置一对多的关联
//modelBuilder.Entity<StudentCourse>()
//.HasOne<Student>(sc => sc.Student)
//.WithMany(s => s.StudentCourses)
//.HasForeignKey(sc => sc.SId);
//modelBuilder.Entity<StudentCourse>()
//.HasOne<Course>(sc => sc.Course)
//.WithMany(s => s.StudentCourses)
//.HasForeignKey(sc => sc.CId);
}
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<StudentCourse> StudentCourses { get; set; }
}
总结一下,在新版本中,EFCore的关联属性的配置都比较简单,
如果非必要的原因也可以遵守它约定的关系配置规则,
总体来说比较难的还是一对一的关联,
有点硬伤是它不能配置单向的一对一的关联.
我的建议是按照约定写,因为我不怎么喜欢这个FluentAPI,它的规则有点复杂,