前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你不一定会用的JPA(Hibernate)的fetch all properties

你不一定会用的JPA(Hibernate)的fetch all properties

作者头像
疯狂软件李刚
发布2020-07-15 15:19:31
1.7K0
发布2020-07-15 15:19:31
举报
文章被收录于专栏:疯狂软件李刚

导读

HQL(JPQL)在执行查询时提供了一个”fetch all properties“选项,乍一看该关键字就不难猜到它的作用就是用于”立即抓取“延迟加载的属性。但如果你试一下这个fetch all properties,你就会发现:这个选项并不如你所想。

本文介绍Hibernate(JPA)基于字节码增强的延迟加载(并非那种简单的延迟加载)的实现,以及fetch all properties的用法

问题出在哪里?

在读者群有一个非常认真的读者(他是一个非常不错、已经从事开发几年的码农),他提出一个问题。

他问的就是《轻量级Java EE企业应用实战》6.4.3节关于fetch的讲解,书上讲到使用“fetch all properties”选项可以立即抓取原本应该延迟加载的属性。

对于“fetch all properties”这个选项,写书为了节省篇幅(总有人抱怨书太厚),就没有专门为它做一个示例。在这种情况下,即使一个已经在企业从事实际开发的读者,想真正掌握这个知识点依然存在一定困难。

实际上我大概能猜到他所做的例子,假设有如下简单的实体。

代码语言:javascript
复制
@Entity
@Table(name = "person_inf")
public class Person
{
    // 定义标识属性
    @Id @Column(name = "person_id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
    // 定义Person实例的name成员变量
    private String name;
    // 定义Person实例的age成员变量
    private int age;
    // 定义一个集合属性
    // 集合属性,保留该对象关联的邮件地址
    @ElementCollection(targetClass = String.class)
    @CollectionTable(name = "person_email",
        joinColumns=@JoinColumn(name = "person_id" , nullable = false))
    @Column(name = "email_detail" , nullable = false)
    private Set<String> emails = new HashSet<>();

    // 省略setter、getter方法
    ...
}

上面Person实体定义了一个emails集合属性,该集合属性默认会使用延迟加载(lazy init)——这是JPA(Hibernate)的默认设定。道理很简单:程序去加载Person实体时,每个Person实体可能存在多个关联的Email地址,因此程序没必要在加载Person实体时,立即加载它关联的全部Email地址。

该实例测试所用的SQL脚本如下:

代码语言:javascript
复制
drop database if exists hibernate;

create database hibernate;

use hibernate;

CREATE TABLE person_inf (
  person_id int(11) PRIMARY KEY auto_increment,
  name longtext default NULL,
  age int(11) default NULL
);

INSERT INTO person_inf VALUES
(1,'crazyit.org', 30),
(2,'孙悟空', 500);

CREATE TABLE person_email (
  person_id int(11) NOT NULL,
  email_detail varchar(255) default NULL,
  FOREIGN KEY (person_id) REFERENCES person_inf (person_id)
);

INSERT INTO person_email VALUES 
(1, 'crazyit@crazyit.org'),
(1, 'crazyit@fkit.com'),
(2, 'sun@163.com'),
(2, 'wukong@163.com');

接下来他可能会提供如下测试代码。

代码语言:javascript
复制
public class HqlQuery {
    public static void main(String[] args)
        throws Exception {
        HqlQuery mgr = new HqlQuery();
        mgr.test1();
    }

    private void test1()throws Exception {
        // 获得Hibernate Session对象
        Session sess = HibernateUtil.currentSession();
        Transaction tx = sess.beginTransaction();
        // 使用fetch all properties选项
        List<Person> pl = sess.createQuery("select p from Person p fetch all properties where p.age = :age", Person.class)
            .setParameter("age", 30)
            .getResultList();
        tx.commit();
        // 关闭Session
        HibernateUtil.closeSession();
        // 遍历结果集
        for (Person p : pl) {
            System.out.println(p.getEmails());
        }
    }
}

上面JPQL(HQL)语句中使用“fetch all properties”选项,他自然而然地以为Hibernate会在查询Person实体时立即抓取它原本该延迟加载的emails属性(集合属性,默认延迟加载)。

如果他运行该程序,不出意外将会看到产生如下错误:

代码语言:javascript
复制
[java] Exception in thread "main" org.hibernate.LazyInitializationException: 
failed to lazily initialize a collection of role: org.crazyit.app.domain.Person.emails, 
could not initialize proxy - no Session

从上面查询代码可以看到:程序在关闭Session之后遍历Person实体,当程序通过Person实体去获取它的集合属性Emails时,由于该属性是延迟加载的——获取延迟加载的属性时需要再次通过Session重新查询,而上面错误正是由于Session被关闭导致的错误,这说明“fetch all properties”选项并不未立即抓取Emails属性。

当然,如果只是单纯地想把该程序改对,那是非常简单的!只要添加“join fetch”即可,只要将程序中createQuery()的JPQL(HQL)改为如下形式:

代码语言:javascript
复制
List<Person> pl = sess.createQuery("select p from Person p fetch all properties join fetch p.emails  where p.age = :age", Person.class)
    .setParameter("age", 30)
    .getResultList();

再次执行程序将可看到JPA(Hibernate)在底层生成如下SQL语句:

代码语言:javascript
复制
select
    person0_.person_id as person_i1_1_,
    person0_.age as age2_1_,
    person0_.name as name3_1_,
    emails1_.person_id as person_i1_0_0__,
    emails1_.email_detail as email_de2_0_0__
from
    person_inf person0_
inner join
    person_email emails1_
        on person0_.person_id=emails1_.person_id
where
    person0_.age=?

看到了吧?如果你希望JPA(Hibernate)在底层使用多表连接语句抓取集合属性(包括关联实体),你需要显式使用"xxx join"或“xxx join fetch”来执行连接,单纯地使用“fetch all properties”选项是不会起作用的。

这是为什么呢?

fetch all properties的作用

答案很简单:“fetch all properties”选项根本就没这功能,它只能帮你预初始化那些原本该延迟加载的属性,它根本不会帮你在底层执行额外的关联查询。

“fetch all properties”选项到底有什么用呢?请仔细看图书6.4.3节的说明。

如果在持久化注解中映射属性时通过指定fetch=FetchType.LAZY启用了延迟加载(这种延迟加载需要通过字节码增强来实现),然后程序里又希望立即初始化那些原本会延迟加载的属性,则可以通过 fetch all properties 来强制Hibernate立即初始化这些属性。 来自《轻量级Java EE企业应用实战》6.4.3, 李刚

书上说的很清楚了,“fetch all properties”只是用于将“延迟加载”改成“立即初始化”——而且该延迟加载还需要通过字节码增强来实现。换而言之,对于JPA(Hibernate)那种简单开启(默认开启或只通过注解)的延迟加载,“fetch all properties”选项是看不到效果。

下面来看看何谓基于字节码增强的延迟加载?它有什么用处?

基于字节码增强的延迟加载

大部分的JPA(hibernate)使用者对延迟加载并不陌生:

  • 默认情况下,对于集合属性或关联实体是多个(1-N或N-N关联)时,JPA(hibernate)自动就会启用延迟加载。
  • 对于复合类型的属性、或关联实体是单个(N-1或1-1)时,也可通过fetch=FetchType.LAZY指定启用延迟加载。

这两种延迟加载,是普通开发者用得最多的延迟加载,它们也不需要使用字节码增强。

试想另外一个种场景下的实体:假设程序中包含一个Document实体,该实体除了包含title(标题)、publishDate(发布时间)……等属性之外,还包含一个content(内容)属性,该属性的只是简单的String类型,但它底层对应的数据列的类型是LONGTEXT或CLOB——总之这种数据类型不是简单的varchar或varchar2之类,它们用于存放大文本对象,其数据量可能高达4GB,这意味着一个Document的content属性值就有可能高达4GB,如果你同时查询100个Document实体,如果JPA(hibernate)在加载这100个Document实体的同时立即加载它的content属性,那必然导致内存溢出!

那么问题来,Document的content属性是否应该延迟加载?应该实现它的延迟加载?

第一个答案很简单:肯定要!必须要!

但第二个答案呢?

提示

很多时候,即使一个看上去很简单的知识点,甚至你以为它没有用处,但实际上它非常重要,但如果你学习的资料不系统、不全面,你只是学习了简单的1+1=2,你学起来固然轻松,但等你真正进入企业开发时,你就发现你会的只是helloworld,实际上你有两点不会:这也不会,那也不会!

相信我:选一本系统、全面、优秀的图书进行学习,看上去难度很大,但实际上才是真正的捷径。

为了让JPA(hibernate)对content属性(String类型)执行延迟加载,此时单纯地靠注解就搞不定了,必须使用基于字节码的延迟加载才行。此时需要两步:

  1. 使用@Basic(fetch = FetchType.LAZY)注解修饰需要延迟加载的标量类型的属性。
  2. 使用Hibernate提供的字节码工具对持久化类执行字节码增强——如果你还记得书中关于AspectJ的介绍,就知道所谓字节码增强,通俗点来说就是修改class文件。

此处就以Person实体的name属性为例(注意SQL脚本中name属性对应列的类型是LONGTEXT),假设程序Person实体的name属性需要使用延迟加载,首先需要将该Person类改为如下形式:

代码语言:javascript
复制
@Entity
@Table(name = "person_inf")
public class Person
{
    // 定义标识属性
    @Id @Column(name = "person_id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
    // 定义Person实例的name成员变量
    @Basic(fetch = FetchType.LAZY)
    private String name;
    // 定义Person实例的age成员变量
    private int age;
    // 定义一个集合属性
    // 集合属性,保留该对象关联的邮件地址
    @ElementCollection(targetClass=String.class)
    @CollectionTable(name="person_email",
        joinColumns=@JoinColumn(name="person_id" , nullable=false))
    @Column(name="email_detail" , nullable=false)
    private Set<String> emails = new HashSet<>();
    // 省略setter、getter方法
    ...
}

注意上面的name属性使用了@Basic(fetch = FetchType.LAZY)修饰。

接下来还需要使用Hibernate提供的org.hibernate.bytecode.enhance.spi.Enhancer来执行字节码增强(也就是修改class文件)。

此处使用一个Ant Task来执行字节码增强,因此在Ant的build.xml文件中增加如下配置:

代码语言:javascript
复制
  <!-- 定义名为enhance的target, 该target依赖compile, 
    因此执行该target之前会自动执行compiletarget -->
  <target name="enhance" depends="compile">
    <!-- 配置一个自定义任务:enhance -->
    <taskdef name="enhance" classname="org.hibernate.tool.enhance.EnhancementTask">
      <classpath refid="classpath"/>
    </taskdef>
    <!-- 定义enhance任务,该任务用于执行字节码增强 -->
    <enhance base="${dest}" dir="${dest}" 
      failOnError="true" 
      enableLazyInitialization="true"
      enableDirtyTracking="true" 
      enableAssociationManagement="true" 
      enableExtendedEnhancement="true"/>
  </target>

上面配置文件的额外定义了一个名为enhance的target,实际上该build.xml文件中还定义了compile和run两个target,其中compile负责编译所有Java源文件,而run则负责运行测试程序所用的主类。

上面配置文件指定了enhance target依赖于compile target,而run target则依赖于enhance target,因此当程序运行run target时,Ant会自动先执行compile和enhance target。

提示

target就是Ant生成文件定义的一个可独立执行的任务。target之间的依赖关系则指定了执行某个target之前需要先执行的其他target。

接下来为程序提供如下测试方法:

代码语言:javascript
复制
    private void test2()throws Exception {
        // 获得Hibernate Session对象
        Session sess = HibernateUtil.currentSession();
        Transaction tx = sess.beginTransaction();
        // 使用fetch all properties选项
        List<Person> pl = sess.createQuery("select p from Person p where p.age = :age", Person.class)
            .setParameter("age", 30)
            .getResultList();
        tx.commit();
        // 关闭Session
        HibernateUtil.closeSession();
        // 遍历结果集
        for (Person p : pl) {
            System.out.println(p.Name());
        }
    }

注意上面JPQL(HQL)中并未使用fetch all properties选项,因此程序查询Person实体(该Preson实体使用了字节码增强)时,程序会对name属性执行延迟加载,这样程序在Session关闭后获取Person实体的name属性将会导致异常。

运行上面test2()测试方法,不出意外将会看到如下错误:

代码语言:javascript
复制
[java] Exception in thread "main" org.hibernate.LazyInitializationException: 
Unable to perform requested lazy initialization [org.crazyit.app.domain.Person.name] 
- no session and settings disallow loading outside the Session

这就“一切尽在掌握中”,要程序出错都在自己掌握中,让它出什么错,它就出什么错误。

此时就可看到“fetch all properties”选项的作用了,在上面JPQL(HQL)中增加该选项,也就是将上面createQuery()的代码改为如下形式:

代码语言:javascript
复制
List<Person> pl = sess.createQuery("select p from Person p fetch all properties where p.age = :age", Person.class)
    .setParameter("age", 30)
    .getResultList();

注意上面JPQL(HQL)增加了“fetch all properties”选项,这样JPA(Hibernate)就会立即初始化name属性(原本应该延迟加载的属性)。

再次运行该上面test2()测试方法,此时将可看到“fetch all properties”选项的作用:程序一切正常。这意味着程序在查询Person实体时立即加载了它的name属性。

最后总结

正如前面提出场景:当实体的某个属性是一个大数据对象时(比如LONGTEXT或CLOB等),此时程序必须对该属性执行延迟加载,否则会导致严重的性能问题。

而@Basic注解和字节码增强结合使用才能让这种属性实现延迟加载。

——这种场景在实际开发中常见吗?太常见了!只要你真正在企业开发,那就肯定会见到这种情况。

而“fetch all properties”选项就是在这种场景下发挥作用的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 疯狂软件李刚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档