在一对多,多对一的关系中"关系"是由多方来维护的,比如教师与班级是一对多的关系,在班级表中使用teacher_id来记录该班级与教师的对应的关系。 ![](https://img.kancloud.cn/6d/81/6d81d77e92740da0afcbaba447fcdaf3_407x125.png) 通过此字段的值不但能够查询出每个班级对应的教师是谁,还能够根据教师ID反向查询出该教师负责哪些班级。 但在多对多的关系中,我们并没有在ER图看到相似的字段。 ![](https://img.kancloud.cn/4d/0b/4d0b167fadb4466a8b473337f09d24a5_182x294.png) 我们即没有在课程实体中发现有班级实体相关的字段,又没有在班级实体中发现有课程相关的字段。其实在多对多的关系中,ER图自动省略了两个实体间的关联表。也就是说班级与课程的多对多关系,真实的反应到数据表中对应ER图如下: ![](https://img.kancloud.cn/f0/21/f0217ec0c458641bdc2055b85447b95e_382x323.png) 而为了更有效的表示两个实体间多对多的关系,会忽略掉用为于存储数据的中间表。在spring中也能够自动生成中间表,在进行数据处理时就好像中间表不存在一样。虽然中间表没有得到有效的表示,但在在程序开发过程中遇到多对多的实际情况,我们要时刻的提醒自己中间表的存在,因为很多多对多关系中"莫名"的问题都是由于处理该中间表的方式不同而引发的。 # @ManyToMany 使用@ManyToMany注解来表示实体间的多对多关系: entity/Course.java ```java public class Course { ... @ManyToMany ➊ private List<Klass>➋ klasses = new ArrayList<>()➌; // 省略getter/setter,请自行添加 ``` * ➊ 使用@ManyToMany注解来表示实体间多对多的关系 * ➋ 多个班级使用List表示 * ➌ 一般会对其进行初始化(这样做有一定的好处) 运行应用,然后使用navicat打开数据库查看生成的中间表的情况: ![](https://img.kancloud.cn/5d/d5/5dd554dc310f3ec5e2b0c02f322e329d_280x72.png) 近一步查看其数据表结构: ![](https://img.kancloud.cn/18/53/18536a8b82c40ff8c2fe51c13856b3a0_568x105.png) 查看索引: ![](https://img.kancloud.cn/de/dc/dedc0d6b6e8532b10eb2eeaf1c322e31_517x102.png) 查看外键: ![](https://img.kancloud.cn/59/39/5939d515eddae572833721cf02c29341_703x106.png) 总结: 1. 自动创建了`表名`+`属性名`的数据表 2. 在数据表中有两个字段,分别为`表名_id`以及`属性名_id`,这两个字段组成了联合主键 3. 同时为两个字段添加了对应的外键及索引 ## 测试 打开测试文件CourseTest,进行多对多测试如下: entity/CourseTest.java ```java ... @Autowired KlassRepository klassRepository; ... @Before public void before() { this.course = new Course(); this.course.setName(RandomString.make(4)); for (int i = 0; i < 2; i++) { Klass klass = new Klass(); klass.setName(RandomString.make(4)); klassRepository.save(klass); this.course.getKlasses().add(klass); ➊ } } ``` * ➊ 将班级添加到课程中 当前项目对数据库的操作策略为:create-drop,即应用启动时生成数据表,应该停止时删除数据表。这样一来,每次单元测试执行完毕后我们都会得到一个空库。虽然这有利于单元测试的成功执行,但却不利于单元测试生成的数据信息,当需要查看单元测试生成的数据信息时,将操作策略暂时修改为:create ![](https://img.kancloud.cn/3f/d5/3fd5162d8cb70d3cdb01ac60a87d2ecc_469x232.png) ``` spring.jpa.hibernate.ddl-auto=create ``` 然后停止刚刚运行的应用,并运行CourseTest中的save方法以测试保存操作。操作完成后查看数据表信息: 新建了两个id分别为1,2的班级: ![](https://img.kancloud.cn/41/46/41462d679f07d91a9c47db8b127aa5d5_240x65.png) 新建了1个id为1的课程: ![](https://img.kancloud.cn/78/82/7882b97cc5653085c8b09cb95c964942_237x45.png) 在中间表中生成了两条关联数据: ![](https://img.kancloud.cn/bb/fa/bbfad907514942368ada63da14c4a01d_219x69.png) 关联数据表示出:id为1的课程对应id为1,2的两个班级。 # 查数据 spring自动添加完多对多的关系后,在进行数据查询时还可以在我们需要的时候自动的查询此多对多数据。CourseTest的save方法中新增代码如下: entity/CourseTest.java ```java import org.springframework.transaction.annotation.Transactional; @Test @Transactional ➊ public void save() { this.courseRepository.save(this.course); Course course = this.courseRepository.findById(this.course.getId()).get(); course.getKlasses(); } ``` * ➊ 为了提升查询的效率,在查询中spring默认不进行多对多数据的读取。若想读取多对多的数据,需要加此注解`@Transactional` >[info] Transactional是设置数据库事务的注解,spring为会每一次的请求自动添加一个事务,而spring的单元测试并不会自动添加此事务。所以在单元测试若要模拟真实的数据库的事务情况,则应该在方法(或类)上添加@Transactional注解。本教程不计划对相关知识进行讲解,在此添加事务仅为了单元测试的演示。 然后在`course.getKlasses();`新增断点,接着使用debug模式来启用测试: ![](https://img.kancloud.cn/54/14/5414e22666a8f967ebd6b8a283ce1ff0_872x167.png) 查看变量信息如下: ![](https://img.kancloud.cn/72/ee/72eea2d4208d2da5bfba8314ab184e4c_528x239.png) # 更新数据 entity/CourseTest.java ```java @Test public void manyToManyUpdate() { // 保存原课程 this.courseRepository.save(this.course); // 新建班级 Klass klass = new Klass(); klass.setName(RandomString.make(6)); klassRepository.save(klass); // 删除已存在的一个班级,新增新班级后更新 this.course.getKlasses().remove(0); this.course.getKlasses().add(klass); this.courseRepository.save(course); } ``` 请自行加入断点进行测试,在过程中注意查看数据表的变化情况。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.7](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.7) | - |