很多朋友在使用Spring Data JPA/JPA/Hibernate的时候,都会发现Entity加载数据的方式难以控制:有的可以修改为Lazy,有的又不可以;有些时候加一些额外注解可以解决,有些时候又不行,看完本文之后,相信大家可以采取一致的方式解决这些问题。
ORM框架的实现机制非常复杂,很多问题是开发者难以想到的,经常会让大家产生疑问。因为国内JPA实践较少,为了让大家更易于理解Hibernate在Fetch数据时的机制,这篇文章尽可能把话题收缩到目标上,尽可能不过多细化问题,也尽可能不引入其他问题。另外,为了让目标更清晰,本文暂不涉及讨论效率优化问题,也不引入数据表。
看完本文之后,会解决大家的如下问题:
1.为什么@ManyToOne和@OneToOne加上FetchType.Lazy仍然无法实现懒加载?
2.Hibernate是如何考虑读取数据的?
3.如何让Entity的数据读取行为变得统一?
4.如何在保持对象关系的情况下控制数据读取?
一个最简单的例子:
public class OTM{
//省略其他字段
@OneToMany(mappedBy = "otm")
private Set<MTO> mtoSet;
//省略后面的方法
}
public class MTO{
//省略其他字段
@ManyToOne
@JoinColumn(name="otm_id",referencedColumnName = "id")
private OTM otm;
//省略后面的方法
}
备注:上述主键默认为id
@OneToMany和@ManyToOne是JPA里用的最多的两个维护关系的注解,如果你用Repository去分别读取OTM和MTO对象会发现:
情况一:读取OTM对象的时候,mtoSet加载方式是Lazy的。如果把@OneToMany加上fetch=FetchType.Eager可以切换为直接加载(当然这会产生第二个Select,本文暂不讨论这个问题)。
情况二:读取MTO对象的时候,otm的加载方式是Eager的,如果设置fetch=FetchType.Lazy会没有效果,仍然会产生第二条Select。
这是为什么?
要回答这个问题,就必须从Hibernate是如何使用代理去解决懒加载的问题开始。最直观的解决方式是把本来的属性替换为代理,覆盖属性本身的所有可访问方法就可以实现,既然可以,但是为什么上述情况2仍然是Eager?
Hibernate里,如果一个非集合属性对应的表数据不存在,那么这个属性会赋null值,如果一个集合属性对应的表数据不存在,那么这个属性会赋为空的Collection(不为null)。
Hibernate原生处理懒加载的方式是用一个代理去替换需要懒加载的属性(请注意,是替换属性本身),并且重写所有可以访问的方法(即第一次调用的时候Fetch数据写入缓存,后面就直接返回缓存的数据了)。但这会因为属性是不是集合(对应OneToMany和ManyToOne关系)会产生两种完全不同的情况。
OneToMany很简单,因为代理是直接替换的属性,如果有数据就填充集合,没有数据就把集合留空就行了。所以,可以把数据是不是为空后置处理,也就产生了上述情况一的结果。
ManyToOne就麻烦了,因为非集合的属性在对应表没有数据的情况下应赋null值,一旦使用代理后就已经不为null了(变成了属性本身不为null但全部字段为null,到底是数据存在还是不存在?万一恰巧对应这张表所有字段都可以为null呢?),代理不能把自己变为null,所以这个没办法后置处理,必须在一开始的就决定是赋null值还是使用代理。这就使得必须直接产生第二条select去判断如何处理。但是既然第二条select都已经发送了,不如直接把数据也填充了。所以,这就产生了上述情况二的结果。相应的,OneToOne情况类似。
所以,从这个层面总结下来,情况很简单:
1.XToOne的关系,都不能懒加载
2.XToMany的关系,都可以懒加载(当然,实际上还有其他考虑)。
这里顺带解释一个问题,如果开发者细心观察Hibernate产生的表会发现带XToX注解的属性映射出来的表中会存在一张表是有外键约束的(前提是引擎innodb)。因为这是判断是否可以懒加载最佳的方式,如果一个对象中存在另外一个对象的外键约束,不就可以提前判断可不可以使用代理了吗?
这个方法在一些hibernate 3.x和4.x版本里是确实可行的(不稳定,经常变),只要加上fetch=FetchType.Lazy的时候加上optional=false,就可以实现前述无法实现的懒加载。这个方法的逻辑很简单,就是告诉hibernate,这个关系是肯定存在的(optional=false,等同于外键约束存在),那当然就可以使用代理去解决懒加载。但这个方法可靠度非常低,因为在实际中,有可能表的默认创建引擎是Mylsam,也可能外键约束建立失败。实际在历史版本中,各种各样的解决方式非常杂乱,但都逐步被抛弃了。
最统一的实践方式如下:
1.不修改XToX的默认fetch方式,说不定你哪天会发现,新版本又不行了,ORM考虑的问题远远不止你能想到的。
2.如果要改变默认的fetch方式,使用Bytecode Enhancement,有Maven插件可以使用。如下:
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>$currentHibernateVersion</version>
<executions>
<execution>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
Bytecode Enhancement的功能很强大,暂时这里只说到这儿。加入插件后,在需要改变默认行为的属性上(前述的XToOne属性)加注解:
@LazyToOne(LazyToOneOption.NO_PROXY)
用Maven编译后,Bytecode Enhancement会对所有Entity使用代理去替换,比如前面的情况二,虽然otm代理本身不能把自己设为null,但是MTO对象可以,所以对MTO用代理替换就解决了这个问题,也就解决了XToOne的懒加载问题。
值得,如果一个非集合属性也需要实现懒加载(比如有一个Lob数据),也是可以通过Bytecode Enhancement实现的,这特别适合某个字段数据特别大的情况。你只需要在属性上加上
@Basic(fetch = FetchType.LAZY)
当然,也有一些其他的变通方式,但容易引入不相关的问题。比如,对于OneToOne这种情况,你也可以把映射OneToOne做成OneToMany来解决,但是这不是很好的实践方式。
最后,在实践ORM的时候,要抛弃Table Driven的开发思路(表关系是基础,对象是实现),转变为以对象为中心的思路(对象关系是基础,数据表是实现),这样才会最大的发挥ORM在后期维护上的优势。