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

當(dāng)我們對其進(jìn)行unique設(shè)置時,jpa會自動在數(shù)據(jù)表中為對應(yīng)的字段添加UNIQUE類型的索引:

但數(shù)據(jù)庫卻并不支持對某個字段設(shè)置其長度必須為多少位,或是其長度必須位于哪兩個值之間。所以此時@JoinColumn及@Column便解決不了這個問題了,這也是當(dāng)我們查看@JoinColumn及@Column官方文檔時,并沒有找到對應(yīng)的選項(xiàng)的原因。
> 這兩個注解中有一個length選項(xiàng),但其官方的解釋為:(Optional) The column length. (Applies only if a string-valued column is used.) ,譯為:字段長度。該長度是指該字段所允許的最大長度,傳入的值只要不超過該值即可。但這并不是我們想要的。
為了處理這種問題,JPA為我們提供了@PrePersist注解,在數(shù)據(jù)正式被保存前,該注解下的方法將被觸發(fā)執(zhí)行1次。
## @PrePersist
我們在entity/Student.java中建立以下方法:
```
/**
* 在實(shí)體保存到數(shù)據(jù)庫以前,執(zhí)行1次
*/
@PrePersist
public void perPersis() {
}
```
### 補(bǔ)充代碼
繼續(xù)補(bǔ)充該方法中的代碼,完成name和sno的長度校驗(yàn)。
```
@Column(nullable = false)
private String name;
/**
* 在實(shí)體保存到數(shù)據(jù)庫以前,執(zhí)行1次
* 1. 校驗(yàn)name 字段長度為2-20
* 2. 校驗(yàn)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進(jìn)行校驗(yàn)
* ② 對sno進(jìn)行校驗(yàn)
* ? 拋出更通用的DataIntegrityViolationException異常,同時在異常中給出有指導(dǎo)意義的提示
## 測試
姓名過短:
```
@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);
}
```
學(xué)號長度非6位:
```
@Test(expected = DataIntegrityViolationException.class)
public void snoLengthTest() {
this.student.setSno("12345");
this.studentRepository.save(student);
}
```
### 增加測試樣本及細(xì)化測試
雖然使用@Test(expected = DataIntegrityViolationException.class)能夠快速的測試異常,但這種方法存在先天的不足,比如:每個測試用例只能測試一次異常。當(dāng)我們需要進(jìn)行多樣本測試的時候,它便顯得力不從心了。在剛剛測試中,我們每個測試用例中均使用了一個樣本。這為我們的后續(xù)更新造成了一定的風(fēng)險(xiǎn)。比如學(xué)號的長度由6位升級為8位,我們來在Student.java中,將6修改為8,卻發(fā)現(xiàn)原來的單元測試仍然被通過了。這是由于我們的單元測試的邏輯為:將學(xué)號為5位時,觸發(fā)異常。而無論學(xué)號的長度是6位還是8位,都會滿足長度不為5的單元測試。而正常的測試邏輯則應(yīng)該是,我們使用多個長度的學(xué)號進(jìn)行測試,僅當(dāng)長度為6時不報(bào)錯。
所以:一個合格的測試應(yīng)該長成這樣:
```
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的字符串,并用此字符串來設(shè)置學(xué)號
* ③ 當(dāng)字符串的長度為6時,斷言未發(fā)生異常
* ④ 當(dāng)字符串的長度不為6時,斷言發(fā)生異常
* ⑤ 生成一個新學(xué)生
此時,如果我們將Student中的長度校驗(yàn)由6改為其它長度時,則單元測試將無法通過。
**請自行完成name字段的長度校驗(yàn)后繼續(xù)學(xué)習(xí)**
### 多測試用例間互相影響
至此我們完成了學(xué)生實(shí)體的校驗(yàn)過程,我們大概寫了10來個單元測試。接下來我們做個奇怪的實(shí)驗(yàn):單獨(dú)運(yùn)行任何一個單元測試均正常通過測試;但統(tǒng)一運(yùn)行該測試文件的所有測試卻發(fā)生了錯誤:

失敗:

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

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

當(dāng)然,這也引發(fā)了一個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();
}
```
最終發(fā)現(xiàn):由于兩次生成的學(xué)生的學(xué)號不一樣了,導(dǎo)致第二次學(xué)生的保存操作時**未**拋出學(xué)號校驗(yàn)異常,我們將此代碼修正如下:
```
@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();
}
```
* ① 生成一個在方法內(nèi)部用的學(xué)號
* ② 在兩次保存學(xué)生前,分別用同一個學(xué)號來對學(xué)生進(jìn)行設(shè)置
此時我們再測試,所有的單元測試便正常通過了 :

# 參考文檔
| 名稱 | 鏈接 | 預(yù)計(jì)學(xué)習(xí)時長(分) |
| --- | --- | --- |
| 源碼地址 | [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
- 第一節(jié):Angular準(zhǔn)備工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二節(jié):Hello Angular
- 第三節(jié):Spring Boot準(zhǔn)備工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四節(jié):Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven國內(nèi)源配置
- 4 package與import
- 第五節(jié):Hello Spring Boot + Angular
- 1 依賴注入【前】
- 2 HttpClient獲取數(shù)據(jù)【前】
- 3 數(shù)據(jù)綁定【前】
- 4 回調(diào)函數(shù)【選學(xué)】
- 第二章 教師管理
- 第一節(jié) 數(shù)據(jù)庫初始化
- 第二節(jié) CRUD之R查數(shù)據(jù)
- 1 原型初始化【前】
- 2 連接數(shù)據(jù)庫【后】
- 3 使用JDBC讀取數(shù)據(jù)【后】
- 4 前后臺對接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節(jié) CRUD之C增數(shù)據(jù)
- 1 新建組件并映射路由【前】
- 2 模板驅(qū)動表單【前】
- 3 httpClient post請求【前】
- 4 保存數(shù)據(jù)【后】
- 5 組件間調(diào)用【前】
- 第四節(jié) CRUD之U改數(shù)據(jù)
- 1 路由參數(shù)【前】
- 2 請求映射【后】
- 3 前后臺對接【前】
- 4 更新數(shù)據(jù)【前】
- 5 更新某個教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節(jié) CRUD之D刪數(shù)據(jù)
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個教師【后】
- 第六節(jié) 代碼重構(gòu)
- 1 文件夾化【前】
- 2 優(yōu)化交互體驗(yàn)【前】
- 3 相對與絕對地址【前】
- 第三章 班級管理
- 第一節(jié) JPA初始化數(shù)據(jù)表
- 第二節(jié) 班級列表
- 1 新建模塊【前】
- 2 初識單元測試【前】
- 3 初始化原型【前】
- 4 面向?qū)ο蟆厩啊?/a>
- 5 測試HTTP請求【前】
- 6 測試INPUT【前】
- 7 測試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺對接【前】
- 第三節(jié) 新增班級
- 1 初始化【前】
- 2 響應(yīng)式表單【前】
- 3 測試POST請求【前】
- 4 JPA插入數(shù)據(jù)【后】
- 5 單元測試【后】
- 6 惰性加載【前】
- 7 對接【前】
- 第四節(jié) 編輯班級
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務(wù)【前】
- 4 測試間諜spy【前】
- 5 使用JPA更新數(shù)據(jù)【后】
- 6 分層開發(fā)【后】
- 7 前后臺對接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節(jié) 選擇教師組件
- 1 初始化【前】
- 2 動態(tài)數(shù)據(jù)綁定【前】
- 3 初識泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識單元測試【前】
- 7 其它問題
- 第六節(jié) 刪除班級
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺對接
- 第四章 學(xué)生管理
- 第一節(jié) 引入Bootstrap【前】
- 第二節(jié) NAV導(dǎo)航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節(jié) footer組件【前】
- 第四節(jié) 歡迎界面【前】
- 第五節(jié) 新增學(xué)生
- 1 初始化【前】
- 2 選擇班級組件【前】
- 3 復(fù)用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗(yàn)【后】
- 7 唯一性校驗(yàn)【后】
- 8 @PrePersist【后】
- 9 CM層開發(fā)【后】
- 10 集成測試
- 第六節(jié) 學(xué)生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識綜合查詢【后】
- 4 綜合查詢進(jìn)階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測試與分頁【前】
- 9 單選與多選【前】
- 10 集成測試
- 第七節(jié) 編輯學(xué)生
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 功能開發(fā)【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗(yàn)證【前】
- 第八節(jié) 刪除學(xué)生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發(fā)與測試【后】
- 4 集成測試
- 5 定制提示框【前】
- 6 引入圖標(biāo)庫【前】
- 第九節(jié) 集成測試
- 第五章 登錄與注銷
- 第一節(jié):普通登錄
- 1 原型【前】
- 2 功能設(shè)計(jì)【前】
- 3 功能設(shè)計(jì)【后】
- 4 應(yīng)用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態(tài)【前】
- 第二節(jié):你是誰
- 1 過濾器【后】
- 2 令牌機(jī)制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個人中心【前】
- 8 攔截器【后】
- 9 集成測試
- 10 單例模式
- 第六章 課程管理
- 第一節(jié) 新增課程
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 async管道【前】
- 4 優(yōu)雅的測試【前】
- 5 功能開發(fā)【前】
- 6 實(shí)體監(jiān)聽器【后】
- 7 @ManyToMany【后】
- 8 集成測試【前】
- 9 異步驗(yàn)證器【前】
- 10 詳解CORS【前】
- 第二節(jié) 課程列表
- 第三節(jié) 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節(jié) 班級列表
- 第節(jié) 教師列表
- 第節(jié) 編輯課程
- TODO返回機(jī)制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節(jié) 刪除課程
- 第七章 權(quán)限管理
- 第一節(jié) AOP
- 總結(jié)
- 開發(fā)規(guī)范
- 備用