深入理解Hibernate中的懶加載異常及解決方法
懶加載異常
寫切面代碼測試的時候發(fā)生了一個異常: LazyInitializationException
@AfterReturning(value = "@annotation(sendWebhookNotification)", returning = "returnValue") @Async public void sendWebHookNotification(SendWebHookNotification sendWebhookNotification, Object returnValue) { }
錯誤信息如下
failed to lazily initialize a collection of role: could not initialize proxy - no Session
這個異常與 hibernate
加載關(guān)聯(lián)對象的2種方式有關(guān),一個是 懶加載,一個是 立即加載
我們知道,hibernate的實(shí)體關(guān)聯(lián)有幾種方式, @OneToOne, @OneToMany, @ManyToOne @ManyToMany
我們查看一下這些注解的屬性
@OneToOne
@Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface OneToOne { ... /** * (Optional) Whether the association should be lazily * loaded or must be eagerly fetched. The EAGER * strategy is a requirement on the persistence provider runtime that * the associated entity must be eagerly fetched. The LAZY * strategy is a hint to the persistence provider runtime. */ FetchType fetch() default EAGER;
@OneToMany
@Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface OneToMany { ... FetchType fetch() default LAZY;
@ManyToOne
@Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface ManyToOne { ... FetchType fetch() default EAGER;
@ManyToMany
@Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface ManyToMany { ... FetchType fetch() default LAZY;
可以發(fā)現(xiàn),需要加載數(shù)量為1的屬性時,加載策略默認(rèn)都是 EAGER, 即立即加載, 如@OneToOne, @ManyToOne。
但是如果需要加載數(shù)量為 n 時,加載策略默認(rèn)都是 LAZY, 即懶加載, 如@OneToMany, @ManyToMany。
原因也很容易想到,如果每一次查詢都加載n方的話,無疑會給數(shù)據(jù)庫帶來壓力。
那么,為什么會發(fā)生懶加載異常呢?
我們把錯誤信息來詳細(xì)看一下
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxx.xxx.xxx, could not initialize proxy - no Session
重點(diǎn)為后面的 no Session
看到session相關(guān)的,我們會想到數(shù)據(jù)庫中的事務(wù)。
先來看一下hibernate執(zhí)行流程:
當(dāng)我們從數(shù)據(jù)庫查詢時,一般會發(fā)生如下事情
- hibernate 開啟一個 session(會話),
- 然后開啟transaction(事務(wù)), 查詢默認(rèn)只讀事務(wù),修改操作需要讀寫事務(wù)
- 接著發(fā)出sql找回數(shù)據(jù)并組裝成pojo(或者說entity、model)
- 這時候如果pojo里有懶加載的對象,并不會去發(fā)出sql查詢db,而是直接返回一個懶加載的代理對象,這個對象里只有id。如果接下來沒有其他的操作去訪問這個代理對象除了id以外的屬性,就不會去初始化這個代理對象,也就不會去發(fā)出sql查找db
- 事務(wù)提交,session 關(guān)閉
如果這時候再去訪問代理對象除了id以外的屬性時,就會報上述的懶加載異常,原因是這時候已經(jīng)沒有session了,無法初始化懶加載的代理對象。
所以為什么會出現(xiàn)no session呢?
是因?yàn)橛昧饲忻妫?還是因?yàn)槲覍ο筠D(zhuǎn)為了Object,或者其他原因?
模擬代碼環(huán)境: 因?yàn)槲矣昧饲忻?,注解,@Async等東西,控制變量測試一下是什么原因?qū)е碌膯栴}
測試:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TestAnnotation { }
@TestAnnotation public List<Training> findAll() { return (List<Training>) this.trainingRepository.findAll(); }
1.測試切面 + 強(qiáng)制 Object 轉(zhuǎn) List 是否會報錯
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { List<Training> list = (List<Training>) returnValue; list.stream().forEach((v) -> { ((Training) v).getNotice().getTrainingResources(); }); list.stream().forEach((v) -> { ((Training) v).getNotice().getNoticeResources(); });
我這里用了 Object 來接收被切函數(shù)的返回值,并強(qiáng)制轉(zhuǎn)換成(List<Training>).
debug 可以看到,即使從Object轉(zhuǎn)換過來,但是運(yùn)行時類型并不會丟失
結(jié)果:不報錯, 說明不是切面和類型的問題。
同樣,測試了轉(zhuǎn)為List<?> 也不會丟失,因?yàn)檫\(yùn)行時類型不變.
2.測試@Async
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") @Async public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { List<?> list = (List<?>) returnValue; list.stream().forEach((v) -> { ((Training) v).getNotice().getTrainingResources(); }); list.stream().forEach((v) -> { ((Training) v).getNotice().getNoticeResources(); });
結(jié)果: 報錯
雖然不是一模一樣的報錯,但是足以說明問題
這時候,我才想起來 @Async會啟用新的線程
而數(shù)據(jù)庫會話通常與線程相關(guān)聯(lián)。當(dāng)一個方法被標(biāo)記為異步并在不同的線程中執(zhí)行時,數(shù)據(jù)庫會話上下文可能不會正確傳播到新的線程。
根據(jù)錯誤原因來解決:
方法1: 在切面之前,就調(diào)用相關(guān)屬性的get方法,也就是說,在沒有進(jìn)入@Async方法之前,就進(jìn)行查庫
@TestAnnotation public List<Training> findAll() { List<Training> list = (List<Training>) this.trainingRepository.findAll(); // 調(diào)用get函數(shù) list.stream().forEach((v) -> { v.getNotice().getTrainingResources(); }); return list; }
方法2: 根據(jù)id, 重新查數(shù)據(jù)庫,建立會話
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { // 重新調(diào)用數(shù)據(jù)庫查詢方法 List<Training> list = (List<Training>) this.trainingRepository.findAllById(((List<Training>)returnValue).stream().map(BaseEntity::getId).collect(Collectors.toList()));
失敗案例:使用:@Transactional(propagation = Propagation.REQUIRES_NEW)
創(chuàng)建新的事務(wù)。
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { List<?> list = (List<?>) returnValue;
猜測可能是因?yàn)樵搶ο蟮拇韺ο髮儆谏弦粋€會話,即使創(chuàng)建新的事務(wù)也不能重新查庫。
源碼分析
可以從源碼的角度看 LazyInitializationException,是如何發(fā)生的。
在組裝pojo時, 會為懶加載對象創(chuàng)建對應(yīng)的代理對象 ,當(dāng)需要獲取該代理對象除id以外的屬性時,就會調(diào)用 AbstractLazyInitializer#initialize()
進(jìn)行初始化
@Override public final void initialize() throws HibernateException { if ( !initialized ) { if ( allowLoadOutsideTransaction ) { permissiveInitialization(); } else if ( session == null ) { throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - no Session" ); } else if ( !session.isOpenOrWaitingForAutoClose() ) { throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - the owning Session was closed" ); } else if ( !session.isConnected() ) { throw new LazyInitializationException( "could not initialize proxy [" + entityName + "#" + id + "] - the owning Session is disconnected" ); } else { target = session.immediateLoad( entityName, id ); initialized = true; checkTargetState(session); } } else { checkTargetState(session); } }
如果這時,session 為null的話,會拋出 LazyInitializationException
。
我們可以看到它有一個例外,那就是 allowLoadOutsideTransaction
為 true 時。
這個變量值true,則可以進(jìn)入 permissiveInitialization()
方法另起session和事務(wù),最終避免懶加載異常。
而當(dāng)我們配置 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
時,
allowLoadOutsideTransaction 就為 true, 從而新建會話。 但是不推薦,這種全局設(shè)置應(yīng)該慎重配置。
倉庫層刪除異常
"No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call"
沒有實(shí)際有效的事務(wù)。
解決: delete方法都需要用@Transactional
public interface TrainingNoticeResourceRepository extends PagingAndSortingRepository<TrainingNoticeResource, Long>, JpaSpecificationExecutor<TrainingNoticeResource> { @Transactional() void deleteAllByTrainingNoticeId(Long id); }
以上就是深入理解Hibernate中的懶加載異常及解決方法的詳細(xì)內(nèi)容,更多關(guān)于Hibernate懶加載異常的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于SpringAOP+Caffeine實(shí)現(xiàn)本地緩存的實(shí)例代碼
公司想對一些不經(jīng)常變動的數(shù)據(jù)做一些本地緩存,我們使用AOP+Caffeine來實(shí)現(xiàn),所以本文給大家介紹了2024-03-03
基于SpringAOP+Caffeine實(shí)現(xiàn)本地緩存的實(shí)例,文中有詳細(xì)的代碼供大家參考,需要的朋友可以參考下Java去重排序之Comparable與Comparator的使用及說明
這篇文章主要介紹了Java去重排序之Comparable與Comparator的使用及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04一次java異步任務(wù)的實(shí)戰(zhàn)記錄
最近做項(xiàng)目的時候遇到了一個小問題,從前臺提交到服務(wù)端A,A調(diào)用服務(wù)端B處理超時,下面這篇文章主要給大家介紹了一次java異步任務(wù)的實(shí)戰(zhàn)記錄,需要的朋友可以參考下2022-05-05java中并發(fā)Queue種類與各自API特點(diǎn)以及使用場景說明
這篇文章主要介紹了java中并發(fā)Queue種類與各自API特點(diǎn)以及使用場景說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06SpringBoot生成License的實(shí)現(xiàn)示例
License指的是版權(quán)許可證,那么對于SpringBoot項(xiàng)目,如何增加License呢?本文就來介紹一下,感興趣的可以了解一下2021-06-06Java service層獲取HttpServletRequest工具類的方法
今天小編就為大家分享一篇關(guān)于Java service層獲取HttpServletRequest工具類的方法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12解決IntellIJ IDEA提示內(nèi)存不足的圖文教程
現(xiàn)在越來越多的人投入了 IntellIJ Idea 的懷抱, 它給我們的日常開發(fā)帶來了諸多便利,但是我們在開發(fā)過程中,總是能碰到idea內(nèi)存不足問題,所以本文給大家介紹了解決IntellIJ IDEA提示內(nèi)存不足的圖文教程,需要的朋友可以參考下2025-03-03