專案執行過程中,一個報錯資訊,報錯資訊如下:

org.hibernate.LazyInitializationException: could not initialize proxy [xxx.domain.Guild#CF12263C600F4BCABC9293D3FABE4B42] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169) ~[hibernate-core-5.3.9.Final.jar!/:5.3.9.Final]
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309) ~[hibernate-core-5.3.9.Final.jar!/:5.3.9.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.3.9.Final.jar!/:5.3.9.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.3.9.Final.jar!/:5.3.9.Final]
at xxx.domain.Guild$HibernateProxy$58NSae2j.getName(Unknown Source) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.task.TaskJiaoFuService.guildName(TaskJiaoFuService.java:181) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.task.TaskJiaoFuService.result2JiaoFuDetail(TaskJiaoFuService.java:122) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.task.TaskJiaoFuService.parseResult(TaskJiaoFuService.java:106) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.task.TaskJiaoFuService.queryV4(TaskJiaoFuService.java:91) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.AbstractExportStrategy.query(AbstractExportStrategy.java:65) ~[classes!/:0.0.9-SNAPSHOT]
at xxx.ExportService.exportAndPersistence(ExportService.java:130) [classes!/:0.0.9-SNAPSHOT]
at xxx.ExportService.lambda$execute$0(ExportService.java:75) [classes!/:0.0.9-SNAPSHOT]
at xxx.common.initialization.ContextCopyingTaskDecorator.lambda$decorate$0(ContextCopyingTaskDecorator.java:20) ~[classes!/:0.0.9-SNAPSHOT]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_181]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_181]
at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_181]

業務很簡單,一個jpa的單表查詢,獲取屬性的時候報錯了

分析

JPA預設使用的懶載入,即使訪問的是單個實體類,返回的物件也是代理,在獲取物件屬性的時候才會進行資料庫查詢,此時如果連線資料session已釋放則會丟擲上述異常

org.hibernate.LazyInitializationException在經常使用hibernate或者jpa的同學中可能經常遇到,網路上一搜,解決問題的方式有很多種,這裡羅列一下:

  • 在spring boot的配置檔案application.properties新增spring.jpa.open-in-view=true
  • 用spring 的OpenSessionInViewFilter
  • 在spring boot的配置檔案application.properties新增spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
  • 在出問題的實體類上加@Proxy(lazy = false)
  • ……

spring.jpa.open-in-view

我們看下baeldung上是怎麼說的,傳送門:https://www.baeldung.com/spring-open-session-in-view

Session per request is a transactional pattern to tie the persistence session and request life-cycles together. Not surprisingly, Spring comes with its own implementation of this pattern, named OpenSessionInViewInterceptor, to facilitate working with lazy associations and therefore, improving developer productivity.

………

By default, OSIV is active in Spring Boot applications. Despite that, as of Spring Boot 2.0, it warns us of the fact that it's enabled at application startup if we haven't configured it explicitly:

spring.jpa.open-in-view is enabled by default. Therefore, database
queries may be performed during view rendering.Explicitly configure
spring.jpa.open-in-view to disable this warning

意思大致是,每個請求會話對於Spring來說都是一種事務模式,所以了我們預設給你開啟了,用於提高開發效率,不過你如果沒有顯式配置的話,我還會給你一個warning告警。關於這個預設配置在github上爭論也有不少:https://github.com/spring-projects/spring-boot/issues/7107

OSIV時序圖如下:

專案中spring.jpa.open-in-view是設定為false的,程式碼獲取實體類或者關聯實體都是service中完成的,一般我們開啟事務,在事務作用的上下文環境中去獲取懶載入的資料是不會有任何問題的,且開啟之後資料的session會等到整個request完成之後才會釋放,其實是十分消耗效能的,之前有其他同學沒有關閉open-in-view遇到的問題:https://www.cnblogs.com/thisismarc/p/13594399.html

結論:spring.jpa.open-in-view為true可以解決報錯,不過不推薦,OpenSessionInViewFilter配置方案也PASS

spring.jpa.properties.hibernate.enable_lazy_load_no_trans

我們也看下baeldung上是怎麼說的,傳送門:https://www.baeldung.com/hibernate-lazy-loading-workaround

While using lazy loading in Hibernate, we might face exceptions, saying there is no session.

……

The recommended approach is to design our application to ensure that data retrieval happens in a single transaction. But, this can sometimes be difficult when using a lazy entity in another part of the code that is unable to determine what has or hasn't been loaded.

Hibernate has a workaround, an enable_lazy_load_no_trans property. Turning this on means that each fetch of a lazy entity will open a temporary session and run inside a separate transaction.

意思大致是,這是一種變通的做法,可以為每個懶載入的實體開啟一個臨時的會話,不過這個方法也是反人類的,因為如果延遲載入的關聯實體越多,請求附加的連線也就越多,這會給資料庫連線帶來壓力。在新事務中載入的每個關聯,在每次關聯初始化後都會強制重新整理事務日誌,所以大大的不建議使用。

@Proxy(lazy = false)

@Proxy(lazy = false)的意思和FetchType.EAGER類似,返回的是初始化好的實體,即關閉了懶載入,這個肯定不是我們想要的

推薦解決方式

回到我們的問題,單表懶載入報錯,專案使用的是Springboot,事務都是顯式的註解配置,查詢的介面我們一般沒有配置@Transactional註解,所以解決方法是在Service的查詢方法上增加 @Transactional(readOnly = true) 註解來劃分事務邊界,這是比較推薦的做法,也複合編碼規範,專案中如果有事務切面配置,把相關方法加到事務控制的範圍中則也不會出現這個問題,如果還有其他更好的方式,歡迎留言

參考連結

https://vladmihalcea.com/the-hibernate-enable_lazy_load_no_trans-anti-pattern/

https://www.baeldung.com/spring-open-session-in-view

https://github.com/spring-projects/spring-boot/issues/7107

https://www.cnblogs.com/thisismarc/p/13594399.html

https://www.baeldung.com/hibernate-lazy-loading-workaround