JPA/hibernate懒加载原理分析及JSON格式API反序列化时连环触发懒加载问题的解决

       什么是懒加载

       JPA是java持久层的API,也就是java官方提供的一个ORM框架,Spring data jpa是spring基于hibernate开发的一个JPA框架。Spring data jpa提供了大量的数据库操作接口,以及采用动态代理的方式做的以接口方法命名的数据库操作方式,大大简化了开发人员对数据库操作的代码。它对于简单的查询操作非常简便,但是一旦涉及到复杂的查询或许就会非常复杂,比如动态字段查询、多表联合查询等,所以很多技术平台都采用了spring data jpa+mybatis的模式,即spring data jpa做简单的查询操作,mybatis做复杂的数据库操作。但是这种模式让我觉得更复杂,因为对于同一张表得提供不同的数据库操作bean,所以我对spring data jpa又做了一层封装,在不去改变源码的基础上,让jpa对于复杂的查询也能非常简便,但这不属于本文讨论的内容,有兴趣的同学可以关注我的公众号,我会在后续文章中专门做深入的讲解剖析。

       所谓的懒加载模式就是只有用到的时候才会去触发加载,不用到的时候就不会触发,在ORM框架中即可以理解为当查询某张表A的数据时,这张表有通过某个字段关联映射到另外一张表B的数据,当需要用到表B的数据时,才以A的关联字段的值去查询表B的相应的数据,如果不需要表B的数据则不用查询,因此懒加载不但简化了代码并且提高了系统性能。至于详细的懒加载用法我就不在此讨论了,网上书上资料非常多,也不属于本文讨论内容。

 

       懒加载原理剖析

       懒加载的大致原理是Spring data jpa在sql查询完成数据映射时,将被标记为懒加载的字段(本文仅讨论一对多、多对多的情况,所以该字段为应为集合)转换成了PersistentCollection类型,我们查看源码或debug跟踪代码即可看到List类型被转换为PersistentBag类型、ArrayList类型被转换为PersistentList类型、HashMap类型被转换为PersistentMap类型等,从图中我们可以看到这些类型都属于PersistentCollection的子类。

       从上图我们可以看到PersistentBag、PersistentList、PersistentMap这些类在实例化时将hibernate的数据库操作核心session做为参数传进来了,你可能会说哪有session啊,那明明是SessionImplementor,我就不多做解释了,我只能说SessionImplementor这个接口是hibernate专门用来抽象为实现org.hibernate.Session这个标准接口所要做的一些子功能。也就是说它本身就包含了数据库的连接、预定义好的sql查询等信息,只等着我们去触发了。

       我们现在知道了懒加载其实是用了“偷梁换柱”的理念,将集合字段替换为自己对应好的类型(如何映射会在后续文章中继续剖析),这个类型中包含了数据库的连接、预定义好的sql,只等着我们主动去触发了,但是如何触发?你可能要说了,我还是按照正常的方式去调用的这个字段,我怎么不知道什么时候触发它了,怎么触发的?对于懒加载字段我们要使用它的时候主要就是在数据返回的时候,也就是页面渲染或者API数据返回。你可能会问那我通过set调用给其他对象的值行吗?不行,在一个事务内hibernate会报集合引用错误,如果事务已经关闭,连接已经释放,也可能会引起其他异常,除非hibernate支持另启动一个外部事务,那仍然会报集合引用错误,所以不可能将懒加载字段赋给其他对象并进行引用。

       现在我们继续讨论该如何触发懒加载,继续查看源码,以PersistentBag为例,发现PersistentBag改写了toString方法,他调用了read方法,而toString方法就是在Debug时、页面渲染或打印中要用到的,这个read方法才是真正的调用入口。read方法中调用了initialize方法进行初始化,完成之后会调用endRead方法,并且将initialized变量设置为true,说明该懒加载字段已经被触发了。我们现在可以梳理通了,在去打印或者渲染懒加载字段时通过toString方法来触发懒加载的执行。

       对于页面渲染部分我们分析完了,那么RestApi是如何触发的呢?RestApi数据返回一般采用json的形式,Spring boot中默认采用jackson进行序列化,我们就以jackson进行简单的分析。跟踪源代码可以看到,jackson在对collection进行序列化时首先调用了size方法来返回总的数据条数,而PersistentBag重写了size方法,在readSize方法中我们又看到了read方法的身影,所有的思路都串起来了!

       我们再来总结一下Spring data jpa懒加载原理,首先它在查询完数据库将数据映射到bean字段时做了偷梁换柱,将集合字段替换为自己组装好的集合对象,并且将hibernate操作数据库的核心Session缓存了进来。在获取该懒加载字段的值时,通过toString或者size方法调用read方式来触发数据库的执行,从而完成对字段懒加载的调用。

 

       API接口序列化时懒加载被连环触发的问题

       现在我们知道懒加载的执行原理了,那么问题来了,在提供API方法时我们通常采用json进行序列化,在json序列化时会循环深度的去调用对象的值来组装json数据,现在假如有一个数据对象A,其中包含对象B,关系是一对多,而对象B又包含C,C又包含D等,都是一对多或多对多关系,在序列化A时取B的数据就牵引到C又牵引到D等,就会引起连环触发,你可能并不需要B或C或D,但是他们确实由于序列化的原因被连环触发了!由于我们平台使用的是jackson进行json序列化,所以我就以jackson为例来提供解决方案了。我们的方案既要保证没有被调用的懒加载字段不被执行,也要保证被调用过的懒加载字段不能被执行。怎样判断懒加载字段是否已经被执行了呢?从上面的分析我们可以看到被执行的懒加载字段的PersistentBag中的初始化字段initialized会被设置为true。那问题就好解决了,我们可以重写序列化方式,判断懒加载字段是否被触发,如果API需要返回某个懒加载字段我们可以通过自己写工具类主动进行触发。

    上述解决方案中涉及到了jackson的自定义序列化、反射及缓存(反射的性能)等相关内容,如果您有不明白的可以通过留言或者关注公众号同我交流,我也会在后续文章中继续提供相关的解决方案,敬请持续关注!


如需获取更多精彩内容或者技术探讨,请扫码关注本人公众号!爱生活,爱代码,代码也是码,我是爱码三疯!

爱码三疯公众号:hlt0912_dyh

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在多线程环境下,如果使用JPA懒加载机制,可能会出现“No session”错误。这是因为每个线程都有自己的事务和Session,当一个线程查询到一个懒加载属性,它会尝试从自己的Session中加载该属性,但是由于该Session已经关闭了,就会出现“No session”错误。 解决问题的方法有两种: 1. 使用EAGER加载方式替代懒加载懒加载属性改为EAGER加载方式,这样在查询主实体就会同查询加载所有关联实体的数据,避免了在后续使用出现“No session”错误。但是这种方式可能会导致性能问题,因为EAGER加载会一次性加载所有关联实体的数据,如果关联实体数据量很大,可能会导致查询性能下降。 2. 在每个线程中重新打开Session 在每个线程中重新打开Session,这样就可以保证每个线程都能访问到自己的Session,避免了“No session”错误。可以使用ThreadLocal来保存每个线程的Session,保证线程安全。示例代码如下: ``` public class JpaUtil { private static ThreadLocal<EntityManager> threadLocal = new ThreadLocal<>(); public static EntityManager getEntityManager() { EntityManager entityManager = threadLocal.get(); if (entityManager == null) { entityManager = EntityManagerFactoryHolder.getEntityManagerFactory().createEntityManager(); threadLocal.set(entityManager); } return entityManager; } public static void closeEntityManager() { EntityManager entityManager = threadLocal.get(); if (entityManager != null) { entityManager.close(); threadLocal.remove(); } } } ``` 使用,在每个线程中打开和关闭Session: ``` public void doInNewThread() { JpaUtil.getEntityManager().getTransaction().begin(); // 查询主实体,关联实体使用懒加载 MainEntity mainEntity = JpaUtil.getEntityManager().find(MainEntity.class, 1L); // 加载关联实体 Hibernate.initialize(mainEntity.getLazyEntity()); JpaUtil.getEntityManager().getTransaction().commit(); JpaUtil.closeEntityManager(); } ``` 这样就可以在多线程环境下使用JPA懒加载机制了。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值