本节我们处理两个长度的校验。在原型中我们规定了学号的长度必须是6位,而姓名则最短为2位,最长为20位。我们在上两个小节中分别通过了@JoinColumn及@Column进行非null及unique设置,这是由于数据库本身就是支持这样的校验的。
当我们对其进行null设置时,jpa会自动在数据表的对应字段上设置`不是null`属性:
![](https://img.kancloud.cn/a5/8c/a58c8aa43282f92b389245ae8b2a1635_769x153.png)
当我们对其进行unique设置时,jpa会自动在数据表中为对应的字段添加UNIQUE类型的索引:
![](https://img.kancloud.cn/31/ba/31baf9653f4a9a83c16193536c604e71_1508x218.png)
但数据库却并不支持对某个字段设置其长度必须为多少位,或是其长度必须位于哪两个值之间。所以此时@JoinColumn及@Column便解决不了这个问题了,这也是当我们查看@JoinColumn及@Column官方文档时,并没有找到对应的选项的原因。
> 这两个注解中有一个length选项,但其官方的解释为:(Optional) The column length. (Applies only if a string-valued column is used.) ,译为:字段长度。该长度是指该字段所允许的最大长度,传入的值只要不超过该值即可。但这并不是我们想要的。
为了处理这种问题,JPA为我们提供了@PrePersist注解,在数据正式被保存前,该注解下的方法将被触发执行1次。
## @PrePersist
我们在entity/Student.java中建立以下方法:
```
/**
* 在实体保存到数据库以前,执行1次
*/
@PrePersist
public void perPersis() {
}
```
### 补充代码
继续补充该方法中的代码,完成name和sno的长度校验。
```
@Column(nullable = false)
private String name;
/**
* 在实体保存到数据库以前,执行1次
* 1. 校验name 字段长度为2-20
* 2. 校验sno 字段长为为6
*/
@PrePersist
public void perPersis() {
if (this.name != null ) { ①
if (this.name.length() < 2) {
throw new DataIntegrityViolationException("name length less than 2"); ➊
}
if (this.name.length() > 20) {
throw new DataIntegrityViolationException("name length more than 20"); ➊
}
}
if (this.sno != null) { ②
if (this.sno.length() != 6) {
throw new DataIntegrityViolationException("sno length must be 6"); ➊
}
}
}
```
* ① 对name进行校验
* ② 对sno进行校验
* ➊ 抛出更通用的DataIntegrityViolationException异常,同时在异常中给出有指导意义的提示
## 测试
姓名过短:
```
@Test(expected = DataIntegrityViolationException.class)
public void nameLengthToShortTest() {
this.student.setName("1");
this.studentRepository.save(student);
}
```
姓名过长:
```
@Test(expected = DataIntegrityViolationException.class)
public void nameLengthToLongTest() {
this.student.setName("123456789012345678901");
this.studentRepository.save(student);
}
```
学号长度非6位:
```
@Test(expected = DataIntegrityViolationException.class)
public void snoLengthTest() {
this.student.setSno("12345");
this.studentRepository.save(student);
}
```
### 增加测试样本及细化测试
虽然使用@Test(expected = DataIntegrityViolationException.class)能够快速的测试异常,但这种方法存在先天的不足,比如:每个测试用例只能测试一次异常。当我们需要进行多样本测试的时候,它便显得力不从心了。在刚刚测试中,我们每个测试用例中均使用了一个样本。这为我们的后续更新造成了一定的风险。比如学号的长度由6位升级为8位,我们来在Student.java中,将6修改为8,却发现原来的单元测试仍然被通过了。这是由于我们的单元测试的逻辑为:将学号为5位时,触发异常。而无论学号的长度是6位还是8位,都会满足长度不为5的单元测试。而正常的测试逻辑则应该是,我们使用多个长度的学号进行测试,仅当长度为6时不报错。
所以:一个合格的测试应该长成这样:
```
import org.assertj.core.internal.bytebuddy.utility.RandomString;
@Test
public void snoLengthTest() {
for (int i = 1; i <= 255; i++) { ①
this.student.setSno(RandomString.make(i)); ②
boolean called = false;
try {
this.studentRepository.save(student);
} catch (DataIntegrityViolationException e) {
called = true;
}
if (i != 6) {
Assertions.assertThat(called).isTrue(); ③
} else {
Assertions.assertThat(called).isFalse(); ④
}
this.before(); ⑤
}
}
```
* ① 测试255次
* ② 获取长度为i的字符串,并用此字符串来设置学号
* ③ 当字符串的长度为6时,断言未发生异常
* ④ 当字符串的长度不为6时,断言发生异常
* ⑤ 生成一个新学生
此时,如果我们将Student中的长度校验由6改为其它长度时,则单元测试将无法通过。
**请自行完成name字段的长度校验后继续学习**
### 多测试用例间互相影响
至此我们完成了学生实体的校验过程,我们大概写了10来个单元测试。接下来我们做个奇怪的实验:单独运行任何一个单元测试均正常通过测试;但统一运行该测试文件的所有测试却发生了错误:
![](https://img.kancloud.cn/1b/f9/1bf904a4e72bf68c1fbcd6ef5b63bce7_493x167.png)
失败:
![](https://img.kancloud.cn/b7/92/b792824b2c95539453b2fe70a10c9e59_412x162.png)
这是由于对某个测试文件进行测试时相当于对该文件中的所有测试文件进行逐个测试,这就会面临多个单元测试用例互相影响的问题。
* [ ] 只运行一个测试用例,该测试用例执行完毕后,JPA自动为我们删除了数据库;再运行另一个测试用例时,数据库为空库。两个测试用例互不影响。
* [ ] 运行一个测试文件,该测试文件中的所有测试用例执行完毕后,此时JPA自动为我们删除了数据库。也就是说在此测试文件中的测试没有全部被执行完前,该测试文件中的测试用例使用的是同一个数据库。这便是产生冲突异常的原因。
我们点击单元测试如下按钮后,将显示各个测试用例的执行顺序:
![](https://img.kancloud.cn/0e/61/0e61c290df02832b0e9ad0516370d3b6_346x364.png)
如上图所示,在执行save操作前已经执行过了snoUniqueTest方法。而该方法中的测试代码曾经在数据表中为我们成功的添加了一个学号为032282的学生;在后续执行save方法时,我们再次尝试在数据表中写入一个学号为032282的学生,此时便发生了唯一性校验错误。解决这个问题的方法也很简单----随机字符串:
我们把before的方法修正如下:
```
this.student.setName("测试名称");
this.student.setSno("032282"); ✘
this.student.setSno(RandomString.make(6)); ✚ ①
this.student.setKlass(this.klass);
```
* ① 每次运行都生成一个随机的学号
然后再测试便可以规避学号互相影响的问题:
![](https://img.kancloud.cn/5b/83/5b83efe5a82f59e9c3df2a2bfbdbbf12_377x388.png)
当然,这也引发了一个snoUniqueTest无法通过的新问题,我们打开该方法再查看一下:
```
@Test
public void snoUniqueTest() {
this.studentRepository.save(this.student);
this.before();
boolean called = false;
try {
this.studentRepository.save(this.student);
} catch (DataIntegrityViolationException e) {
called = true;
}
Assertions.assertThat(called).isTrue();
}
```
最终发现:由于两次生成的学生的学号不一样了,导致第二次学生的保存操作时**未**抛出学号校验异常,我们将此代码修正如下:
```
@Test
public void snoUniqueTest() {
String sno = RandomString.make(6); ①
this.student.setSno(sno); ②
this.studentRepository.save(this.student);
this.before();
this.student.setSno(sno); ②
boolean called = false;
try {
this.studentRepository.save(this.student);
} catch (DataIntegrityViolationException e) {
called = true;
}
Assertions.assertThat(called).isTrue();
}
```
* ① 生成一个在方法内部用的学号
* ② 在两次保存学生前,分别用同一个学号来对学生进行设置
此时我们再测试,所有的单元测试便正常通过了 :
![](https://img.kancloud.cn/8e/86/8e86892d4f3670aa16d993a8870c727b_344x365.png)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.8](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.8) | - |
| PerPersist | [https://docs.oracle.com/javaee/7/api/javax/persistence/PrePersist.html](https://docs.oracle.com/javaee/7/api/javax/persistence/PrePersist.html) | 2 |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用