导读
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”这个选项,写书为了节省篇幅(总有人抱怨书太厚),就没有专门为它做一个示例。在这种情况下,即使一个已经在企业从事实际开发的读者,想真正掌握这个知识点依然存在一定困难。
实际上我大概能猜到他所做的例子,假设有如下简单的实体。
@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脚本如下:
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');
接下来他可能会提供如下测试代码。
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属性(集合属性,默认延迟加载)。
如果他运行该程序,不出意外将会看到产生如下错误:
[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)改为如下形式:
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语句:
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)使用者对延迟加载并不陌生:
这两种延迟加载,是普通开发者用得最多的延迟加载,它们也不需要使用字节码增强。
试想另外一个种场景下的实体:假设程序中包含一个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类型)执行延迟加载,此时单纯地靠注解就搞不定了,必须使用基于字节码的延迟加载才行。此时需要两步:
此处就以Person实体的name属性为例(注意SQL脚本中name属性对应列的类型是LONGTEXT),假设程序Person实体的name属性需要使用延迟加载,首先需要将该Person类改为如下形式:
@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文件中增加如下配置:
<!-- 定义名为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。
接下来为程序提供如下测试方法:
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()测试方法,不出意外将会看到如下错误:
[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()的代码改为如下形式:
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”选项就是在这种场景下发挥作用的。