深入理解Hibernate中的懒加载异常及解决方法

 更新时间:2023年10月15日 14:34:49   作者:weiweiyi  
这篇文章主要为大家介绍了深入理解Hibernate中的懒加载异常及解决方法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪<BR>

懒加载异常

写切面代码测试的时候发生了一个异常: 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懒加载异常的资料请关注脚本之家其它相关文章!

相关文章

  • 基于SpringAOP+Caffeine实现本地缓存的实例代码

    基于SpringAOP+Caffeine实现本地缓存的实例代码

    公司想对一些不经常变动的数据做一些本地缓存,我们使用AOP+Caffeine来实现,所以本文给大家介绍了
    基于SpringAOP+Caffeine实现本地缓存的实例,文中有详细的代码供大家参考,需要的朋友可以参考下
    2024-03-03
  • Java去重排序之Comparable与Comparator的使用及说明

    Java去重排序之Comparable与Comparator的使用及说明

    这篇文章主要介绍了Java去重排序之Comparable与Comparator的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • 一次java异步任务的实战记录

    一次java异步任务的实战记录

    最近做项目的时候遇到了一个小问题,从前台提交到服务端A,A调用服务端B处理超时,下面这篇文章主要给大家介绍了一次java异步任务的实战记录,需要的朋友可以参考下
    2022-05-05
  • java中并发Queue种类与各自API特点以及使用场景说明

    java中并发Queue种类与各自API特点以及使用场景说明

    这篇文章主要介绍了java中并发Queue种类与各自API特点以及使用场景说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • SpringBoot生成License的实现示例

    SpringBoot生成License的实现示例

    License指的是版权许可证,那么对于SpringBoot项目,如何增加License呢?本文就来介绍一下,感兴趣的可以了解一下
    2021-06-06
  • Java service层获取HttpServletRequest工具类的方法

    Java service层获取HttpServletRequest工具类的方法

    今天小编就为大家分享一篇关于Java service层获取HttpServletRequest工具类的方法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • JDK17安装教程以及其环境变量配置教程

    JDK17安装教程以及其环境变量配置教程

    环境变量对Java初学者来说真的是一件头疼的事,本人也经历过这样的事情,这篇文章主要给大家介绍了关于JDK17安装教程以及其环境变量配置的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-03-03
  • 解决IntellIJ IDEA提示内存不足的图文教程

    解决IntellIJ IDEA提示内存不足的图文教程

    现在越来越多的人投入了 IntellIJ Idea 的怀抱, 它给我们的日常开发带来了诸多便利,但是我们在开发过程中,总是能碰到idea内存不足问题,所以本文给大家介绍了解决IntellIJ IDEA提示内存不足的图文教程,需要的朋友可以参考下
    2025-03-03
  • Eclipse项目有红感叹号的解决方法

    Eclipse项目有红感叹号的解决方法

    这篇文章主要为大家详细介绍了Eclipse项目有红感叹号的解决方法,给出了Eclipse项目有红感叹号的原因,以及如何解决?,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • 拳皇(Java简单的小程序)代码实例

    拳皇(Java简单的小程序)代码实例

    这篇文章主要介绍了拳皇Java简单小程序,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03

最新评论