一次 Spring Data JPA 查询返回数据属性为 null 排查

现象

现象非常奇怪,同一查询,在其他方法中正常,但是在这个方法中 JSR 303 Bean 校验没有通过,查看后发现返回的所有数据域均为 null,见下图。

数据库里数据是存在的,其他地方的调用返回的数据是正常的,比如下面这里。

这两者之前的调用也都是类似的,查询用户信息,其中用户信息实体与错误代码实体以 @ManyToOne 关联并启用了延迟加载,如下:

排查

现象非常奇怪,两个很类似的的操作,一个返回数据正常,一个返回的数据域均为 null。

偶然尝试把用户信息实体中的懒加载替换为立即加载 FetchType.EAGER,问题就不再出现了,但是仍然不知道根本原因。

很明显这里并没有直接用到延迟加载,错误代码是直接加载的数据库数据,日志打印也能证明这一点,但是关掉延迟加载后就正常,理论上报错的代码查出的数据等于延迟加载的这条数据,所以怀疑是不是延迟加载导致缓存中应有的数据未加载,而二次查询时没打到数据库而是直接访问的 Hibernate 缓存,延迟加载也失效了,从而导致二次查询数据域均为 null,但是这么来说的话又解释不了另一个查询为什么是正常的。

先尝试单步跟一下代码。

看到这里就比较有意思了,明明数据已经能拉出来了,但是在 Hibernate Interceptor 中,并未复制给对应的属性。其次,可以看到得到的对象并不是真正的实体对象,而是实体的代理。

我们来对比一个正常的调用

很明显,这里的域有值而异常的则没有,而且这里得到的是真正的实体对象而非代理。异常的多了一个 $$_hibernate_interceptor 属性,该属性内嵌的属性包含了所需要的数据。

问题点就在这个 Hibernate Interceptor 中。

突然想起来,正确的那个调用在拉取用户信息实体时,是根本加载不到实体的,因为这是一个针对注册的接口,拉出的实体直接就是空的。

而下方有问题的调用,是对于已经注册的用户,问题代码前面的操作是能够拉取到用户实体的,并且也包含了我们错误代码所需的数据,并且执行的是懒加载,而因为是懒加载,导致 Hibernate 创建了代理对象,但是并没有实际数据。

这样就能解释为什么第一个调用正常而第二个却有了报错。

改为立即加载后,数据都到了内存,也就解释了最开始推理上的矛盾点。

顺着上面的推理,现在的问题就是,为什么对于不同仓库层的可以说是没什么关联的 SQL,Hibernate 共享了缓存数据,根据实体 ID 吗?

下面就是研究一下 Hibernate 的缓存策略了。

参考一下这位老哥的 博文,重点是通过 ID 来进行缓存的,以及 StackOverflow,重点是 open-in-view 导致返回了 session 缓存中的代理对象。

问题到这里已经很清晰的,打开了 open-in-view 让 Hibernate 共享的 session 缓存导致得到的是个代理对象,JSR 303 又直接用反射拿的域属性,导致校验失败了。

能想到的修改办法:

  • 关闭 open-in-view,之前所有涉及到延迟加载的地方都可能会涉及到 no session 的问题,需要手动添加 @Transactional 注解维持 session。改动量太大,而且一些拆分的方法合到一起会变得很难看,其次对以后的代码结构约束太多
  • 关闭对于实体层的 JSR 303 校验。不可能不做数据校验的,不考虑
  • 所有延迟加载的地方都修改为立即加载。性能影响太大,不考虑
  • 修改对于实体,将 JSR 303 Bean 校验移至 Getter 方法上而非域上。只需修改实体注解,移除 Lombok,手动生成 Getter Setter,并把相应的 JSR 303 注解移到 Getter 上

选择了最后一种。

后话

Williams 老师诚不欺我,很早之前就说再 Getter 上进行各种注解才是一种最佳实践,后面为了方便加上 Lombok 的大行其道,基本都注解在域上了,导致花了整整一晚才找到根本不该出现问题。

已经凌晨一点了,Hello World!

posted @ 2021-09-05 20:03  seliote  阅读(589)  评论(1编辑  收藏  举报