JPA 複合主鍵重複情境
情境說明
目前 JPA 幾乎是 Hibernate 框架的主流應用,內建 ORM 功能,並支援基礎的 findById、findAll 方法。
這週在支援其他案子時遇到一個奇怪的問題:使用 JPA 取資料時,結果出現了數筆重複資料。直覺上會以為是轉換成 DTO 時重複 set 資料導致,但實際觀察發現,有些資料會重複、有些不會。進一步追查才發現問題出在:使用複合主鍵 @Id + @IdClass 的情況下,主鍵值發生了重複。
原因來自 SA 設計資料表時,誤將 txn_time 當作唯一鍵值的一部分,實際資料卻沒有保證唯一性。
在理想情況下,Table 設計中複合主鍵應該構成一組真正的 PRIMARY KEY 或 UNIQUE KEY。然而理想很豐滿,現實很骨感,特別是新舊系統交接、資料遷移(Data Migration)時,常會出現這類不一致問題。
模擬資料
1 | -- 建立資料表,且不設定 primary key |
Entity 與複合主鍵
1 |
|
1 |
|
呼叫 findAll() 取得資料
1 |
|
1 | [ |
你會發現重複的主鍵資料,JPA 只會保留第一筆,導致資料丟失或重複。
即使改用 JPQL 或 Native Query?
1 |
|
依然會有重複問題!
Hibernate/JPA 的底層解析
Hibernate 背後維護一個 Persistence Context(即 Entity 一級快取),裡面用如下結構儲存 Entity:
1 | Map<Serializable, Object> entitiesById; |
這裡的 key 就是你設定的主鍵(@Id 或 @IdClass),value 是對應的 Entity。
每當 Hibernate 從資料庫取出一筆 row,它會:
- 讀取主鍵組合
- 檢查是否已經有相同主鍵的 Entity 在快取中
- 有的話 -> 跳過 or 覆蓋
- 沒有的話 -> 加入 Map
所以如果主鍵欄位值重複,只會保留第一筆或最後一筆。
解法:使用 DTO 接資料
若無法變更資料庫設計(或不能加上真正的主鍵/唯一鍵),最佳解法就是不用 Entity 當回傳型別,而是用自訂的 DTO 或 VO 來承接結果。
使用 JPQL constructor expression
1 |
|
接著用這些 DTO 組出回傳資料:
1 | List<TxnLogDto> dtoList = txnLogRepository.findAllTxnLogToDto(); |
結果就會正確地包含每一筆資料,即使主鍵值重複,也不會被覆蓋掉,因為 JPA 不會把 DTO 當成持久化實體處理,也就沒有主鍵比對與快取問題。
這類問題其實在系統設計初期就可以避免,但實務上在資料遷移、系統整併或接手舊案時,仍很容易踩到坑。
若沒遇過這種狀況,Debug 起來真的會很崩潰,尤其是一直以為是 lambda、轉 DTO、list 重複 add 導致,然後卡在那裏核對快一個小時。
剛好這次遇到,把案例記錄下來。