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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180910G01RUD00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券