零、前言
為了方便描述,我們將專案進行一下抽象和簡化。
這是一個前端用Angular、後端用Spring的專案,專案E-R圖的其中一段如下:
不難看出,鄉鎮和社群是1對多的關係。
在管理較低階的區域時,需要關聯到較高階區域的外來鍵(例如:社群必須有一個所屬的鄉鎮)
由於這幾種區域的查詢都很頻繁,為減少SQL頻率,在後臺設定了快取。
為了避免刪除資料導致整個系統的錯誤,全域性啟用了軟刪除。
一、問題復現
在任何一個實體的管理頁面(如鄉鎮管理)中刪除一個物件(鄉鎮),列表中不再顯示刪除的資料,資料庫中也能看到,已刪除物件的deleted=1:
但是在其他實體關聯查詢時(社群中設定鄉鎮時),確可以查到已經刪除的物件,而且居然還能儲存...
(如果儲存已刪除的資料,會導致系統報錯)
簡單總結一下就是:
由於專案程式碼的某些問題,軟刪除在列表分頁查詢時正常,但到了需要外來鍵關聯時,軟刪除卻失效了。
二、排查問題
排除快取原因
首先從issue上看,可能是後端的快取導致的(之前出現過類似的問題)。後端設定了重新登入時清除快取,因此測試很簡單。
嘗試了瀏覽器重新整理、退出重新登入、換瀏覽器等操作,並沒有解決問題,現在基本上排除快取原因了。
進一步排查,Spring中使用debug模式步進查詢功能的內部程式碼,發現返回值中出現了被刪除的資訊
至此可以斷定不是快取問題而是查詢方法的問題。
檢查呼叫關係
既然是查詢出了問題,為什麼在鄉鎮列表卻可以正常區分已刪除的資料呢?
帶著疑問,我找到了前後端的呼叫關係:
鄉鎮列表發起的分頁查詢,最終呼叫到findAll方法
而在社群->鄉鎮選擇器中查詢鄉鎮,最終也會呼叫到findAll方法,但引數不同
// findAll沒有引數
@Override
public List<Town> findAll() {
return (List<Town>) this.townRepository.findAll();
}
// page有引數
@Override
public Page<Town> page(String name, Pageable pageable) {
return this.townRepository.findAll(TownSpecs.containingName(name)), pageable);
}
於是初步判斷,是倉庫層TownRepository的getAll()方法漏寫了軟刪除相關的功能導致的,但目前我們看到的程式碼中,並沒有關於軟刪除是如何實現的,所以繼續找。
探索軟刪除的實現
既然已經知道問題出在哪,接下來就去找,在這個專案中,軟刪除是怎麼實現的,以及影響倉庫層查詢的關鍵的程式碼在哪裡,我從歷史的Pull Request中找到軟刪除的PR。
發現本專案中,所有的實體都繼承了基礎實體,啟用軟刪除需要在基礎實體中設定deleted和deleteAt欄位,以及相關的Setter、Getter方法,用來表示已刪除和刪除時間:
然後在所有的繼承類上新增@SQLDelete註解,把刪除功能替換成”設定deleted=1“
第二行@where(clause = "deleted = false") 作用是在查詢時只查詢沒有被軟刪除的資料。
此時我想到了一個笨方法:在倉庫層所有的findAll上增加deleted = 0 的條件,但問題是,這麼多的實體,會產生大量重複程式碼,而且也沒有從根本上解決問題,因此放棄。
至此,找了一圈還是沒找到答案:按理說這樣已經可以生效了,但為什麼findAll()會不正常呢?
又比對了一下本地最新版本的程式碼,發現繼承實體中已經刪去了@where(clause = "deleted = false")
正當我納悶的時候,發現程式碼註釋裡有一個思否連結,開啟一看正是潘老師之前寫的軟刪除的部落格,於是我又讀了一遍。
spring boot實現軟刪除
這才瞭解到:
@Where(clause = "deleted = false")會導致我們在進行all或page查詢時,得到一個500 EntiyNotFound錯誤。
部落格中也給出瞭解決方法:建立一個SoftDeleteCrudRepository介面,繼承並覆蓋JPA內部的CrudRepository的方法,手動的為查詢方法新增deleted = false條件(具體程式碼見部落格),這樣既能實現軟刪除又避免了500錯誤。
三、解開BUG的神祕面紗
所以可以猜測到,問題一定是出現在我們自己寫的SoftDeleteCrudRepository上,大概是因為某些方法沒有override。
來到此專案的軟刪除倉庫中,關於findAll的過載方法有這些:
@Override
public Page<T> findAll(Pageable pageable) {
return this.findAll(this.andDeleteFalseSpecification(null), pageable);
}
@Override
public Page<T> findAll(@Nullable Specification<T> specification, Pageable pageable) {
return super.findAll(this.andDeleteFalseSpecification(specification), pageable);
}
@Override
public List<T> findAll(@Nullable Specification<T> specification, Sort sort) {
return super.findAll(this.andDeleteFalseSpecification(specification), sort);
}
我們再來回顧一下,剛才的兩種情況是怎麼呼叫的:
// findAll沒有引數
@Override
public List<Town> findAll() {
return (List<Town>) this.townRepository.findAll();
}
// page有引數
@Override
public Page<Town> page(String name, Pageable pageable) {
return this.townRepository.findAll(TownSpecs.containingName(name)), pageable);
}
好,破案了,我們的軟刪除類中並沒有override空引數情況的findAll方法,因此對於空引數,並沒有自動加上deleted=1 的查詢條件。
所以只需要在這裡加上:
@Override
public List<T> findAll(@Nullable Specification<T> specification) {
return super.findAll(this.andDeleteFalseSpecification(specification));
}
就可以讓本文的bug在所有倉庫的findAll()方法中消失,日後再出現新的呼叫方式,也只需修改軟刪除類即可。
至此問題終於解決。
參考資料
- spring boot實現軟刪除:https://segmentfault.com/a/11...
後記
其實這個專案真正的 寫法比參考資料中更復雜:
- SoftDeleteCrudRepository不再是介面,而是實現類
- 其他倉庫並不是直接繼承SoftDeleteCrudRepository,而是使用工廠模式注入
- 專案中工廠模式的程式碼我沒看懂,其實最後也沒搞明白,其他倉庫在沒有extends也沒有implements的情況下,是怎麼呼叫SoftDeleteCrudRepository的。
版權宣告
本文作者:河北工業大學夢雲智開發團隊 - 劉宇軒
新人經驗不足,有建議歡迎交流,有錯誤歡迎輕噴