本節我們處理兩個長度的校驗。在原型中我們規定了學號的長度必須是6位,而姓名則最短為2位,最長為20位。我們在上兩個小節中分別通過了@JoinColumn及@Column進行非null及unique設置,這是由于數據庫本身就是支持這樣的校驗的。
當我們對其進行null設置時,jpa會自動在數據表的對應字段上設置`不是null`屬性:

當我們對其進行unique設置時,jpa會自動在數據表中為對應的字段添加UNIQUE類型的索引:

但數據庫卻并不支持對某個字段設置其長度必須為多少位,或是其長度必須位于哪兩個值之間。所以此時@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來個單元測試。接下來我們做個奇怪的實驗:單獨運行任何一個單元測試均正常通過測試;但統一運行該測試文件的所有測試卻發生了錯誤:

失敗:

這是由于對某個測試文件進行測試時相當于對該文件中的所有測試文件進行逐個測試,這就會面臨多個單元測試用例互相影響的問題。
* [ ] 只運行一個測試用例,該測試用例執行完畢后,JPA自動為我們刪除了數據庫;再運行另一個測試用例時,數據庫為空庫。兩個測試用例互不影響。
* [ ] 運行一個測試文件,該測試文件中的所有測試用例執行完畢后,此時JPA自動為我們刪除了數據庫。也就是說在此測試文件中的測試沒有全部被執行完前,該測試文件中的測試用例使用的是同一個數據庫。這便是產生沖突異常的原因。
我們點擊單元測試如下按鈕后,將顯示各個測試用例的執行順序:

如上圖所示,在執行save操作前已經執行過了snoUniqueTest方法。而該方法中的測試代碼曾經在數據表中為我們成功的添加了一個學號為032282的學生;在后續執行save方法時,我們再次嘗試在數據表中寫入一個學號為032282的學生,此時便發生了唯一性校驗錯誤。解決這個問題的方法也很簡單----隨機字符串:
我們把before的方法修正如下:
```
this.student.setName("測試名稱");
this.student.setSno("032282"); ?
this.student.setSno(RandomString.make(6)); ? ①
this.student.setKlass(this.klass);
```
* ① 每次運行都生成一個隨機的學號
然后再測試便可以規避學號互相影響的問題:

當然,這也引發了一個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://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
- 總結
- 開發規范
- 備用