Spring Data JPA/JPA/Hibernate 进阶教程(一)Entity数据加载最佳实践

很多朋友在使用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在后期维护上的优势。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容