深入理解Hibernate中的懶加載異常及解決方法
懶加載異常
寫(xiě)切面代碼測(cè)試的時(shí)候發(fā)生了一個(gè)異常: LazyInitializationException
@AfterReturning(value = "@annotation(sendWebhookNotification)", returning = "returnValue") @Async public void sendWebHookNotification(SendWebHookNotification sendWebhookNotification, Object returnValue) { }
錯(cuò)誤信息如下
failed to lazily initialize a collection of role: could not initialize proxy - no Session
這個(gè)異常與 hibernate
加載關(guān)聯(lián)對(duì)象的2種方式有關(guān),一個(gè)是 懶加載,一個(gè)是 立即加載
我們知道,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的屬性時(shí),加載策略默認(rèn)都是 EAGER, 即立即加載, 如@OneToOne, @ManyToOne。
但是如果需要加載數(shù)量為 n 時(shí),加載策略默認(rèn)都是 LAZY, 即懶加載, 如@OneToMany, @ManyToMany。
原因也很容易想到,如果每一次查詢都加載n方的話,無(wú)疑會(huì)給數(shù)據(jù)庫(kù)帶來(lái)壓力。
那么,為什么會(huì)發(fā)生懶加載異常呢?
我們把錯(cuò)誤信息來(lái)詳細(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)的,我們會(huì)想到數(shù)據(jù)庫(kù)中的事務(wù)。
先來(lái)看一下hibernate執(zhí)行流程:
當(dāng)我們從數(shù)據(jù)庫(kù)查詢時(shí),一般會(huì)發(fā)生如下事情
- hibernate 開(kāi)啟一個(gè) session(會(huì)話),
- 然后開(kāi)啟transaction(事務(wù)), 查詢默認(rèn)只讀事務(wù),修改操作需要讀寫(xiě)事務(wù)
- 接著發(fā)出sql找回?cái)?shù)據(jù)并組裝成pojo(或者說(shuō)entity、model)
- 這時(shí)候如果pojo里有懶加載的對(duì)象,并不會(huì)去發(fā)出sql查詢db,而是直接返回一個(gè)懶加載的代理對(duì)象,這個(gè)對(duì)象里只有id。如果接下來(lái)沒(méi)有其他的操作去訪問(wèn)這個(gè)代理對(duì)象除了id以外的屬性,就不會(huì)去初始化這個(gè)代理對(duì)象,也就不會(huì)去發(fā)出sql查找db
- 事務(wù)提交,session 關(guān)閉
如果這時(shí)候再去訪問(wèn)代理對(duì)象除了id以外的屬性時(shí),就會(huì)報(bào)上述的懶加載異常,原因是這時(shí)候已經(jīng)沒(méi)有session了,無(wú)法初始化懶加載的代理對(duì)象。
所以為什么會(huì)出現(xiàn)no session呢?
是因?yàn)橛昧饲忻妫?還是因?yàn)槲覍?duì)象轉(zhuǎn)為了Object,或者其他原因?
模擬代碼環(huán)境: 因?yàn)槲矣昧饲忻妫⒔?,@Async等東西,控制變量測(cè)試一下是什么原因?qū)е碌膯?wèn)題
測(cè)試:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TestAnnotation { }
@TestAnnotation public List<Training> findAll() { return (List<Training>) this.trainingRepository.findAll(); }
1.測(cè)試切面 + 強(qiáng)制 Object 轉(zhuǎn) List 是否會(huì)報(bào)錯(cuò)
@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 來(lái)接收被切函數(shù)的返回值,并強(qiáng)制轉(zhuǎn)換成(List<Training>).
debug 可以看到,即使從Object轉(zhuǎn)換過(guò)來(lái),但是運(yùn)行時(shí)類型并不會(huì)丟失
結(jié)果:不報(bào)錯(cuò), 說(shuō)明不是切面和類型的問(wèn)題。
同樣,測(cè)試了轉(zhuǎn)為L(zhǎng)ist<?> 也不會(huì)丟失,因?yàn)檫\(yùn)行時(shí)類型不變.
2.測(cè)試@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é)果: 報(bào)錯(cuò)
雖然不是一模一樣的報(bào)錯(cuò),但是足以說(shuō)明問(wèn)題
這時(shí)候,我才想起來(lái) @Async會(huì)啟用新的線程
而數(shù)據(jù)庫(kù)會(huì)話通常與線程相關(guān)聯(lián)。當(dāng)一個(gè)方法被標(biāo)記為異步并在不同的線程中執(zhí)行時(shí),數(shù)據(jù)庫(kù)會(huì)話上下文可能不會(huì)正確傳播到新的線程。
根據(jù)錯(cuò)誤原因來(lái)解決:
方法1: 在切面之前,就調(diào)用相關(guān)屬性的get方法,也就是說(shuō),在沒(méi)有進(jìn)入@Async方法之前,就進(jìn)行查庫(kù)
@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ù)庫(kù),建立會(huì)話
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { // 重新調(diào)用數(shù)據(jù)庫(kù)查詢方法 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;
猜測(cè)可能是因?yàn)樵搶?duì)象的代理對(duì)象屬于上一個(gè)會(huì)話,即使創(chuàng)建新的事務(wù)也不能重新查庫(kù)。
源碼分析
可以從源碼的角度看 LazyInitializationException,是如何發(fā)生的。
在組裝pojo時(shí), 會(huì)為懶加載對(duì)象創(chuàng)建對(duì)應(yīng)的代理對(duì)象 ,當(dāng)需要獲取該代理對(duì)象除id以外的屬性時(shí),就會(huì)調(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); } }
如果這時(shí),session 為null的話,會(huì)拋出 LazyInitializationException
。
我們可以看到它有一個(gè)例外,那就是 allowLoadOutsideTransaction
為 true 時(shí)。
這個(gè)變量值true,則可以進(jìn)入 permissiveInitialization()
方法另起session和事務(wù),最終避免懶加載異常。
而當(dāng)我們配置 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
時(shí),
allowLoadOutsideTransaction 就為 true, 從而新建會(huì)話。 但是不推薦,這種全局設(shè)置應(yīng)該慎重配置。
倉(cāng)庫(kù)層刪除異常
"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"
沒(méi)有實(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懶加載異常的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于SpringAOP+Caffeine實(shí)現(xiàn)本地緩存的實(shí)例代碼
公司想對(duì)一些不經(jīng)常變動(dòng)的數(shù)據(jù)做一些本地緩存,我們使用AOP+Caffeine來(lái)實(shí)現(xiàn),所以本文給大家介紹了2024-03-03
基于SpringAOP+Caffeine實(shí)現(xiàn)本地緩存的實(shí)例,文中有詳細(xì)的代碼供大家參考,需要的朋友可以參考下Java去重排序之Comparable與Comparator的使用及說(shuō)明
這篇文章主要介紹了Java去重排序之Comparable與Comparator的使用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04一次java異步任務(wù)的實(shí)戰(zhàn)記錄
最近做項(xiàng)目的時(shí)候遇到了一個(gè)小問(wèn)題,從前臺(tái)提交到服務(wù)端A,A調(diào)用服務(wù)端B處理超時(shí),下面這篇文章主要給大家介紹了一次java異步任務(wù)的實(shí)戰(zhàn)記錄,需要的朋友可以參考下2022-05-05java中并發(fā)Queue種類與各自API特點(diǎn)以及使用場(chǎng)景說(shuō)明
這篇文章主要介紹了java中并發(fā)Queue種類與各自API特點(diǎn)以及使用場(chǎng)景說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06SpringBoot生成License的實(shí)現(xiàn)示例
License指的是版權(quán)許可證,那么對(duì)于SpringBoot項(xiàng)目,如何增加License呢?本文就來(lái)介紹一下,感興趣的可以了解一下2021-06-06Java service層獲取HttpServletRequest工具類的方法
今天小編就為大家分享一篇關(guān)于Java service層獲取HttpServletRequest工具類的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12解決IntellIJ IDEA提示內(nèi)存不足的圖文教程
現(xiàn)在越來(lái)越多的人投入了 IntellIJ Idea 的懷抱, 它給我們的日常開(kāi)發(fā)帶來(lái)了諸多便利,但是我們?cè)陂_(kāi)發(fā)過(guò)程中,總是能碰到idea內(nèi)存不足問(wèn)題,所以本文給大家介紹了解決IntellIJ IDEA提示內(nèi)存不足的圖文教程,需要的朋友可以參考下2025-03-03Eclipse項(xiàng)目有紅感嘆號(hào)的解決方法
這篇文章主要為大家詳細(xì)介紹了Eclipse項(xiàng)目有紅感嘆號(hào)的解決方法,給出了Eclipse項(xiàng)目有紅感嘆號(hào)的原因,以及如何解決?,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04