在一对多,多对一的关系中"关系"是由多方来维护的,比如教师与班级是一对多的关系,在班级表中使用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) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用