一次 Spring Data JPA 查詢返回數據屬性為 null 排查


現象

現象非常奇怪,同一查詢,在其他方法中正常,但是在這個方法中 JSR 303 Bean 校驗沒有通過,查看后發現返回的所有數據域均為 null,見下圖。

數據庫里數據是存在的,其他地方的調用返回的數據是正常的,比如下面這里。

這兩者之前的調用也都是類似的,查詢用戶信息,其中用戶信息實體與錯誤代碼實體以 @ManyToOne 關聯並啟用了延遲加載,如下:

排查

現象非常奇怪,兩個很類似的的操作,一個返回數據正常,一個返回的數據域均為 null。

偶然嘗試把用戶信息實體中的懶加載替換為立即加載 FetchType.EAGER,問題就不再出現了,但是仍然不知道根本原因。

很明顯這里並沒有直接用到延遲加載,錯誤代碼是直接加載的數據庫數據,日志打印也能證明這一點,但是關掉延遲加載后就正常,理論上報錯的代碼查出的數據等於延遲加載的這條數據,所以懷疑是不是延遲加載導致緩存中應有的數據未加載,而二次查詢時沒打到數據庫而是直接訪問的 Hibernate 緩存,延遲加載也失效了,從而導致二次查詢數據域均為 null,但是這么來說的話又解釋不了另一個查詢為什么是正常的。

先嘗試單步跟一下代碼。

看到這里就比較有意思了,明明數據已經能拉出來了,但是在 Hibernate Interceptor 中,並未復制給對應的屬性。其次,可以看到得到的對象並不是真正的實體對象,而是實體的代理。

我們來對比一個正常的調用

很明顯,這里的域有值而異常的則沒有,而且這里得到的是真正的實體對象而非代理。異常的多了一個 $$_hibernate_interceptor 屬性,該屬性內嵌的屬性包含了所需要的數據。

問題點就在這個 Hibernate Interceptor 中。

突然想起來,正確的那個調用在拉取用戶信息實體時,是根本加載不到實體的,因為這是一個針對注冊的接口,拉出的實體直接就是空的。

而下方有問題的調用,是對於已經注冊的用戶,問題代碼前面的操作是能夠拉取到用戶實體的,並且也包含了我們錯誤代碼所需的數據,並且執行的是懶加載,而因為是懶加載,導致 Hibernate 創建了代理對象,但是並沒有實際數據。

這樣就能解釋為什么第一個調用正常而第二個卻有了報錯。

改為立即加載后,數據都到了內存,也就解釋了最開始推理上的矛盾點。

順着上面的推理,現在的問題就是,為什么對於不同倉庫層的可以說是沒什么關聯的 SQL,Hibernate 共享了緩存數據,根據實體 ID 嗎?

下面就是研究一下 Hibernate 的緩存策略了。

參考一下這位老哥的 博文,重點是通過 ID 來進行緩存的,以及 StackOverflow,重點是 open-in-view 導致返回了 session 緩存中的代理對象。

問題到這里已經很清晰的,打開了 open-in-view 讓 Hibernate 共享的 session 緩存導致得到的是個代理對象,JSR 303 又直接用反射拿的域屬性,導致校驗失敗了。

能想到的修改辦法:

  • 關閉 open-in-view,之前所有涉及到延遲加載的地方都可能會涉及到 no session 的問題,需要手動添加 @Transactional 注解維持 session。改動量太大,而且一些拆分的方法合到一起會變得很難看,其次對以后的代碼結構約束太多
  • 關閉對於實體層的 JSR 303 校驗。不可能不做數據校驗的,不考慮
  • 所有延遲加載的地方都修改為立即加載。性能影響太大,不考慮
  • 修改對於實體,將 JSR 303 Bean 校驗移至 Getter 方法上而非域上。只需修改實體注解,移除 Lombok,手動生成 Getter Setter,並把相應的 JSR 303 注解移到 Getter 上

選擇了最后一種。

后話

Williams 老師誠不欺我,很早之前就說再 Getter 上進行各種注解才是一種最佳實踐,后面為了方便加上 Lombok 的大行其道,基本都注解在域上了,導致花了整整一晚才找到根本不該出現問題。

已經凌晨一點了,Hello World!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM