深入理解Hibernate中的懒加载异常及解决方法
作者:weiweiyi
懒加载异常
写切面代码测试的时候发生了一个异常: 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
加载关联对象的2种方式有关,一个是 懒加载,一个是 立即加载
我们知道,hibernate的实体关联有几种方式, @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;
可以发现,需要加载数量为1的属性时,加载策略默认都是 EAGER, 即立即加载, 如@OneToOne, @ManyToOne。
但是如果需要加载数量为 n 时,加载策略默认都是 LAZY, 即懒加载, 如@OneToMany, @ManyToMany。
原因也很容易想到,如果每一次查询都加载n方的话,无疑会给数据库带来压力。
那么,为什么会发生懒加载异常呢?
我们把错误信息来详细看一下
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxx.xxx.xxx, could not initialize proxy - no Session
重点为后面的 no Session
看到session相关的,我们会想到数据库中的事务。
先来看一下hibernate执行流程:
当我们从数据库查询时,一般会发生如下事情
- hibernate 开启一个 session(会话),
- 然后开启transaction(事务), 查询默认只读事务,修改操作需要读写事务
- 接着发出sql找回数据并组装成pojo(或者说entity、model)
- 这时候如果pojo里有懒加载的对象,并不会去发出sql查询db,而是直接返回一个懒加载的代理对象,这个对象里只有id。如果接下来没有其他的操作去访问这个代理对象除了id以外的属性,就不会去初始化这个代理对象,也就不会去发出sql查找db
- 事务提交,session 关闭
如果这时候再去访问代理对象除了id以外的属性时,就会报上述的懒加载异常,原因是这时候已经没有session了,无法初始化懒加载的代理对象。
所以为什么会出现no session呢?
是因为用了切面, 还是因为我将对象转为了Object,或者其他原因?
模拟代码环境: 因为我用了切面,注解,@Async等东西,控制变量测试一下是什么原因导致的问题
测试:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TestAnnotation { }
@TestAnnotation public List<Training> findAll() { return (List<Training>) this.trainingRepository.findAll(); }
1.测试切面 + 强制 Object 转 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 来接收被切函数的返回值,并强制转换成(List<Training>).
debug 可以看到,即使从Object转换过来,但是运行时类型并不会丢失
结果:不报错, 说明不是切面和类型的问题。
同样,测试了转为List<?> 也不会丢失,因为运行时类型不变.
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(); });
结果: 报错
虽然不是一模一样的报错,但是足以说明问题
这时候,我才想起来 @Async会启用新的线程
而数据库会话通常与线程相关联。当一个方法被标记为异步并在不同的线程中执行时,数据库会话上下文可能不会正确传播到新的线程。
根据错误原因来解决:
方法1: 在切面之前,就调用相关属性的get方法,也就是说,在没有进入@Async方法之前,就进行查库
@TestAnnotation public List<Training> findAll() { List<Training> list = (List<Training>) this.trainingRepository.findAll(); // 调用get函数 list.stream().forEach((v) -> { v.getNotice().getTrainingResources(); }); return list; }
方法2: 根据id, 重新查数据库,建立会话
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { // 重新调用数据库查询方法 List<Training> list = (List<Training>) this.trainingRepository.findAllById(((List<Training>)returnValue).stream().map(BaseEntity::getId).collect(Collectors.toList()));
失败案例:使用:@Transactional(propagation = Propagation.REQUIRES_NEW)
创建新的事务。
@AfterReturning(value = "@annotation(TestAnnotation)", returning = "returnValue") @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void testAspect(TestAnnotation TestAnnotation, Object returnValue) { List<?> list = (List<?>) returnValue;
猜测可能是因为该对象的代理对象属于上一个会话,即使创建新的事务也不能重新查库。
源码分析
可以从源码的角度看 LazyInitializationException,是如何发生的。
在组装pojo时, 会为懒加载对象创建对应的代理对象 ,当需要获取该代理对象除id以外的属性时,就会调用 AbstractLazyInitializer#initialize()
进行初始化
@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,则可以进入 permissiveInitialization()
方法另起session和事务,最终避免懒加载异常。
而当我们配置 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
时,
allowLoadOutsideTransaction 就为 true, 从而新建会话。 但是不推荐,这种全局设置应该慎重配置。
仓库层删除异常
"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"
没有实际有效的事务。
解决: delete方法都需要用@Transactional
public interface TrainingNoticeResourceRepository extends PagingAndSortingRepository<TrainingNoticeResource, Long>, JpaSpecificationExecutor<TrainingNoticeResource> { @Transactional() void deleteAllByTrainingNoticeId(Long id); }
以上就是深入理解Hibernate中的懒加载异常及解决方法的详细内容,更多关于Hibernate懒加载异常的资料请关注脚本之家其它相关文章!