Hibernate 延迟加载 分析

出处:http://www.ibm.com/developerworks/cn/java/j-lo-hibernatelazy/#icommentshtml

 

Hibernate 的延迟加载(lazy load)是一个被普遍使用的技术。这种延迟加载保证了应用只有在须要时才去数据库中抓取相应的记录。经过延迟加载技术能够避免过多、过早地加载数据表里 的数据,从而下降应用的内存开销。Hibernate 的延迟加载本质上就是代理模式的应用,当程序经过 Hibernate 装载一个实体时,默认状况下,Hibernate 并不会当即抓取它的集合属性、关联实体因此对应的记录,而是经过生成一个代理来表示这些集合属性、关联实体,这就是代理模式应用带来的优点。java

Hibernate 延迟加载剖析与代理模式应用

Hibernate 的延迟加载(lazy load)是一个被普遍使用的技术。这种延迟加载保证了应用只有在须要时才去数据库中抓取相应的记录。经过延迟加载技术能够避免过多、过早地加载数据表里 的数据,从而下降应用的内存开销。Hibernate 的延迟加载本质上就是代理模式的应用,当程序经过 Hibernate 装载一个实体时,默认状况下,Hibernate 并不会当即抓取它的集合属性、关联实体因此对应的记录,而是经过生成一个代理来表示这些集合属性、关联实体,这就是代理模式应用带来的优点。Hibernae 的延迟加载是一个很是经常使用的技术,实体的集合属性默认会被延迟加载,实体所关联的实体默认也会被延迟加载。Hibernate 经过这种延迟加载来下降系统的内存开销,从而保证 Hibernate 的运行性能。程序员

下面先来剖析 Hibernate 延迟加载的“秘密”。web

集合属性的延迟加载

当 Hibernate 从数据库中初始化某个持久化实体时,该实体的集合属性是否随持久化类一块儿初始化呢?若是集合属性里包含十万,甚至百万的记录,在初始化持久化实体的同时, 完成全部集合属性的抓取,将致使性能急剧降低。彻底有可能系统只须要使用持久化类集合属性中的部分记录,而彻底不是集合属性的所有,这样,没有必要一次加 载全部的集合属性。数据库

对于集合属性,一般推荐使用延迟加载策略。所谓延迟加载就是等系统须要使用集合属性时才从数据库装载关联的数据。编程

例以下面 Person 类持有一个集合属性,该集合属性里的元素的类型为 Address,该 Person 类的代码片断以下:设计模式

清单 1. Person.java
 public class Person 
 { 
 // 标识属性
 private Integer id; 
 // Person 的 name 属性
 private String name; 
 // 保留 Person 的 age 属性
 private int age; 
 // 使用 Set 来保存集合属性
 private Set<Address> addresses = new HashSet<Address>(); 
 // 下面省略了各属性的 setter 和 getter 方法
 ... 
 }

为了让 Hibernate 能管理该持久化类的集合属性,程序为该持久化类提供以下映射文件:网络

清单 2. Person.hbm.xml
 <?xml version="1.0" encoding="GBK"?> 
 <!DOCTYPE hibernate-mapping PUBLIC 
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> 
 <hibernate-mapping package="org.crazyit.app.domain"> 
 <!-- 映射 Person 持久化类 --> 
 <class name="Person" table="person_inf"> 
 <!-- 映射标识属性 id --> 
 <id name="id" column="person_id"> 
 <!-- 定义主键生成器策略 --> 
 <generator class="identity"/> 
 </id> 
 <!-- 用于映射普通属性 --> 
 <property name="name" type="string"/> 
 <property name="age" type="int"/> 
 <!-- 映射集合属性  --> 
 <set name="addresses" table="person_address" lazy="true"> 
 <!-- 指定关联的外键列 --> 
 <key column="person_id"/> 
 <composite-element class="Address"> 
 <!-- 映射普通属性 detail --> 
 <property name="detail"/> 
 <!-- 映射普通属性 zip --> 
 <property name="zip"/> 
 </composite-element> 
 </set> 
 </class> 
 </hibernate-mapping>

从上面映射文件的代码能够看出,Person 的集合属性中的 Address 类只是一个普通的 POJO。该 Address 类里包含 detail、zip 两个属性。因为 Address 类代码很是简单,故此处再也不给出该类的代码。session

上面映射文件中 <set.../> 元素里的代码指定了 lazy="true"(对于 <set.../> 元素来讲,lazy="true"是默认值),它指定 Hibernate 会延迟加载集合属性里 Address 对象。架构

例如经过以下代码来加载 ID 为 1 的 Person 实体:

 Session session = sf.getCurrentSession(); 
 Transaction tx = session.beginTransaction(); 
 Person p = (Person) session.get(Person.class, 1);  //<1> 
 System.out.println(p.getName());

上面代码只是须要访问 ID 为 1 的 Person 实体,并不想访问这个 Person 实体所关联的 Address 对象。此时有两种状况:

  • 若是不延迟加载,Hibernate 就会在加载 Person 实体对应的数据记录时当即抓取它关联的 Address 对象。
  • 若是采用延迟加载,Hibernate 就只加载 Person 实体对应的数据记录。

很明显,第二种作法既能减小与数据库的交互,并且避免了装载 Address 实体带来的内存开销——这也是 Hibernate 默认启用延迟加载的缘由。

如今的问题是,延迟加载究竟是如何实现的呢? Hibernate 在加载 Person 实体时,Person 实体的 addresses 属性值是什么呢?

为了解决这个问题,咱们在 <1>号代码处设置一个断点,在 Eclipse 中进行 Debug,此时能够看到 Eclipse 的 Console 窗口有如图 1 所示的输出:

图 1. 延迟加载集合属性的 Console 输出
图 1. 延迟加载集合属性的 Console 输出

正如图 1 输出所看到的,此时 Hibernate 只从 Person 实体对应的数据表中抓取数据,并未从 Address 对象对应的数据表中抓取数据,这就是延迟加载。

那么 Person 实体的 addresses 属性是什么呢?此时能够从 Eclipse 的 Variables 窗口看到如图 2 所示的结果:

图 2. 延迟加载的集合属性值
图 2. 延迟加载的集合属性值

从图 2 的方框里的内容能够看出,这个 addresses 属性并非咱们熟悉的 HashSet、TreeSet 等实现类,而是一个 PersistentSet 实现类,这是 Hibernate 为 Set 接口提供的一个实现类。

PersistentSet 集合对象并未真正抓取底层数据表的数据,所以天然也没法真正去初始化集合里的 Address 对象。不过 PersistentSet 集合里持有一个 session 属性,这个 session 属性就是 Hibernate Session,当程序须要访问 PersistentSet 集合元素时,PersistentSet 就会利用这个 session 属性去抓取实际的 Address 对象对应的数据记录。

那么到底抓取那些 Address 实体对应的数据记录呢?这也难不倒 PersistentSet,由于 PersistentSet 集合里还有一个 owner 属性,该属性就说明了 Address 对象所属的 Person 实体,Hibernate 就会去查找 Address 对应数据表中外键值参照到该 Person 实体的数据。

例如咱们单击图 2 所示窗口中 addresses 行,也就是告诉 Eclipse 要调试、输出 addresses 属性,这就是要访问 addresses 属性了,此时就能够在 Eclipse 的 Console 窗口看到输出以下 SQL 语句:

    select 
        addresses0_.person_id as person1_0_0_, 
        addresses0_.detail as detail0_, 
        addresses0_.zip as zip0_ 
    from 
        person_address addresses0_ 
    where 
        addresses0_.person_id=?

这就是 PersistentSet 集合跟据 owner 属性去抓取特定 Address 记录的 SQL 语句。此时能够从 Eclipse 的 Variables 窗口看到图 3 所示的输出:

图 3. 已加载的集合属性值
图 3. 已加载的集合属性值

从图 3 能够看出,此时的 addresses 属性已经被初始化了,集合里包含了 2 个 Address 对象,这正是 Person 实体所关联的两个 Address 对象。

通 过上面介绍能够看出,Hibernate 对于 Set 属性延迟加载关键就在于 PersistentSet 实现类。在延迟加载时,开始 PersistentSet 集合里并不持有任何元素。但 PersistentSet 会持有一个 Hibernate Session,它能够保证当程序须要访问该集合时“当即”去加载数据记录,并装入集合元素。

与 PersistentSet 实现类相似的是,Hibernate 还提供了 PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 等实现类,它们的功能与 PersistentSet 的功能大体相似。

熟悉 Hibernate 集合属性读者应该记得:Hibernate 要求声明集合属性只能用 Set、List、Map、SortedSet、SortedMap 等接口,而不能用 HashSet、ArrayList、HashMap、TreeSet、TreeMap 等实现类,其缘由就是由于 Hibernate 须要对集合属性进行延迟加载,而 Hibernate 的延迟加载是依靠 PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、 PersistentSortedSet 来完成的——也就是说,Hibernate 底层须要使用本身的集合实现类来完成延迟加载,所以它要求开发者必须用集合接口、而不是集合实现类来声明集合属性。

Hibernate 对集合属性默认采用延迟加载,在某些特殊的状况下,为 <set.../>、<list.../>、<map.../> 等元素设置 lazy="false"属性来取消延迟加载。

关联实体的延迟加载

默认状况下,Hibernate 也会采用延迟加载来加载关联实体,无论是一对多关联、仍是一对一关联、多对多关联,Hibernate 默认都会采用延迟加载。

对于关联实体,能够将其分为两种状况:

  • 关联实体是多个实体时(包括一对多、多对多):此时关联实体将以集合的形式存在,Hibernate 将使用 PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、 PersistentSortedSet 等集合来管理延迟加载的实体。这就是前面所介绍的情形。
  • 关联实体是单个实体时(包括一对1、多对一):当 Hibernate 加载某个实体时,延迟的关联实体将是一个动态生成代理对象。

当关联实体是单个实体时,也就是使用 <many-to-one.../> 或 <one-to-one.../> 映射关联实体的情形,这两个元素也可经过 lazy 属性来指定延迟加载。

下面例子把 Address 类也映射成持久化类,此时 Address 类也变成实体类,Person 实体与 Address 实体造成一对多的双向关联。此时的映射文件代码以下:

清单 3. Person.hbm.xml
 <?xml version="1.0" encoding="GBK"?> 
 <!-- 指定 Hibernate 的 DTD 信息 --> 
 <!DOCTYPE hibernate-mapping PUBLIC 
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> 
 <hibernate-mapping package="org.crazyit.app.domain"> 
 <!-- 映射 Person 持久化类 --> 
 <class name="Person" table="person_inf"> 
 <!-- 映射标识属性 id --> 
 <id name="id" column="person_id"> 
 <!-- 定义主键生成器策略 --> 
 <generator class="identity"/> 
 </id> 
 <!-- 用于映射普通属性 --> 
 <property name="name" type="string"/> 
 <property name="age" type="int"/> 
 <!-- 映射集合属性,集合元素是其余持久化实体
没有指定 cascade 属性,指定不控制关联关系 --> 
 <set name="addresses" inverse="true"> 
 <!-- 指定关联的外键列 --> 
 <key column="person_id"/> 
 <!-- 用以映射到关联类属性 --> 
 <one-to-many class="Address"/> 
 </set> 
 </class> 

 <!-- 映射 Address 持久化类 --> 
 <class name="Address" table="address_inf"> 
 <!-- 映射标识属性 addressId --> 
 <id name="addressId" column="address_id"> 
 <!-- 指定主键生成器策略 --> 
 <generator class="identity"/> 
 </id> 
 <!-- 映射普通属性 detail --> 
 <property name="detail"/> 
 <!-- 映射普通属性 zip --> 
 <property name="zip"/> 
 <!-- 必须指定列名为 person_id, 
与关联实体中 key 元素的 column 属性值相同 --> 
 <many-to-one name="person" class="Person"
 column="person_id" not-null="true"/> 
 </class> 
 </hibernate-mapping>

接下来程序经过以下代码片断来加载 ID 为 1 的 Person 实体:

 // 打开上下文相关的 Session 
 Session session = sf.getCurrentSession(); 
 Transaction tx = session.beginTransaction(); 
 Address address = (Address) session.get(Address.class , 1); //<1> 
 System.out.println(address.getDetail());

为了看到 Hibernate 加载 Address 实体时对其关联实体的处理,咱们在 <1>号代码处设置一个断点,在 Eclipse 中进行 Debug,此时能够看到 Eclipse 的 Console 窗口输出以下 SQL 语句:

    select 
        address0_.address_id as address1_1_0_, 
        address0_.detail as detail1_0_, 
        address0_.zip as zip1_0_, 
        address0_.person_id as person4_1_0_ 
    from 
        address_inf address0_ 
    where 
        address0_.address_id=?

从这条 SQL 语句不难看出,Hibernate 加载 Address 实体对应的数据表抓取记录,并未从 Person 实体对应的数据表中抓取记录,这是延迟加载发挥了做用。

从 Eclipse 的 Variables 窗口看到如图 4 所示的输出:

图 4. 延迟加载的实体
图 4. 延迟加载的实体

从 图 4 能够清楚地看到,此时 Address 实体所关联的 Person 实体并非 Person 对象,而是一个 Person_$$_javassist_0 类的实例,这个类是 Hibernate 使用 Javassist 项目动态生成的代理类——当 Hibernate 延迟加载关联实体时,将会采用 Javassist 生成一个动态代理对象,这个代理对象将负责代理“暂未加载”的关联实体。

只要应用程序须要使用“暂未加载”的关联实体,Person_$$_javassist_0 代理对象会负责去加载真正的关联实体,并返回实际的关联实体——这就是最典型的代理模式。

单击图 4 所示 Variables 窗口中的 person 属性(也就是在调试模式下强行使用 person 属性),此时看到 Eclipse 的 Console 窗口输出以下的 SQL 语句:

    select 
        person0_.person_id as person1_0_0_, 
        person0_.name as name0_0_, 
        person0_.age as age0_0_ 
    from 
        person_inf person0_ 
    where 
        person0_.person_id=?

上面 SQL 语句就是去抓取“延迟加载”的关联实体的语句。此时能够看到 Variables 窗口输出图 5 所示的结果:

图 5. 已加载的实体
图 5. 已加载的实体

Hibernate 采用“延迟加载”管理关联实体的模式,其实就在加载主实体时,并未真正去抓取关联实体对应数据,而只是动态地生成一个对象做为关联实体的代理。当应用程序真正须要使用关联实体时,代理对象会负责从底层数据库抓取记录,并初始化真正的关联实体。

在 Hibernate 的延迟加载中,客户端程序开始获取的只是一个动态生成的代理对象,而真正的实体则委托给代理对象来管理——这就是典型的代理模式。

代理模式

代理模式是一种应用很是普遍的设计模式,当客户端代码须要调用某个对象时,客户端实际上也不关心是否准确获得该对象,它只要一个能提供该功能的对象便可,此时咱们就可返回该对象的代理(Proxy)。

在这种设计方式下,系统会为某个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个 Java 对象表明另外一个 Java 对象来采起行动。在某些状况下,客户端代码不想或不可以直接调用被调用者,代理对象能够在客户和目标对象之间起到中介的做用。

对客户端而言,它不能分辨出代理对象与真实对象的区别,它也无须分辨代理对象和真实对象的区别。客户端代码并不知道真正的被代理对象,客户端代码面向接口编程,它仅仅持有一个被代理对象的接口。

总而言之,只要客户端代码不能或不想直接访问被调用对象——这种状况有不少缘由,好比须要建立一个系统开销很大的对象,或者被调用对象在远程主机上,或者目标对象的功能还不足以知足需求……,而是额外建立一个代理对象返回给客户端使用,那么这种设计方式就是代理模式。

下面示范一个简单的代理模式,程序首先提供了一个 Image 接口,表明大图片对象所实现的接口,该接口代码以下:

清单 3. Image.java
 public interface Image 
 { 
 void show(); 
 }

该接口提供了一个实现类,该实现类模拟了一个大图片对象,该实现类的构造器使用 Thread.sleep() 方法来暂停 3s。下面是该 BigImage 的程序代码。

清单 4. BigImage.java
 // 使用该 BigImage 模拟一个很大图片
 public class BigImage implements Image 
 { 
 public BigImage() 
 { 
 try 
 { 
 // 程序暂停 3s 模式模拟系统开销 
				 Thread.sleep(3000); 
 System.out.println("图片装载成功 ..."); 
 } 
 catch (InterruptedException ex) 
 { 
 ex.printStackTrace(); 
 } 
 } 
 // 实现 Image 里的 show() 方法
 public void show() 
 { 
 System.out.println("绘制实际的大图片"); 
 } 
 }

上面的程序代码暂停了 3s,这代表建立一个 BigImage 对象须要 3s 的时间开销——程序使用这种延迟来模拟装载此图片所致使的系统开销。若是不采用代理模式,当程序中建立 BigImage 时,系统将会产生 3s 的延迟。为了不这种延迟,程序为 BigImage 对象提供一个代理对象,BigImage 类的代理类以下所示。

清单 5. ImageProxy.java
 public class ImageProxy implements Image 
 { 
 // 组合一个 image 实例,做为被代理的对象
 private Image image; 
 // 使用抽象实体来初始化代理对象
 public ImageProxy(Image image) 
 { 
 this.image = image; 
 } 
 /** 
 * 重写 Image 接口的 show() 方法
 * 该方法用于控制对被代理对象的访问,
 * 并根据须要负责建立和删除被代理对象
 */ 
 public void show() 
 { 
 // 只有当真正须要调用 image 的 show 方法时才建立被代理对象
 if (image == null) 
 { 
  image = new BigImage(); 
  } 
 image.show(); 
 } 
 }

上面的 ImageProxy 代理类实现了与 BigImage 相同的 show() 方法,这使得客户端代码获取到该代理对象以后,能够将该代理对象当成 BigImage 来使用。

在 ImageProxy 类的 show() 方法中增长了控制逻辑,这段控制逻辑用于控制当系统真正调用 image 的 show() 时,才会真正建立被代理的 BigImage 对象。下面程序须要使用 BigImage 对象,但程序并非直接返回 BigImage 实例,而是先返回 BigImage 的代理对象,以下面程序所示。

清单 6. BigImageTest.java
 public class BigImageTest 
 { 
 public static void main(String[] args) 
 { 
 long start = System.currentTimeMillis(); 
 // 程序返回一个 Image 对象,该对象只是 BigImage 的代理对象
 Image image = new ImageProxy(null); 
 System.out.println("系统获得 Image 对象的时间开销 :" + 
 (System.currentTimeMillis() - start)); 
 // 只有当实际调用 image 代理的 show() 方法时,程序才会真正建立被代理对象。
 image.show(); 
 } 
 }

上面程序初始化 image 很是快,由于程序并未真正建立 BigImage 对象,只是获得了 ImageProxy 代理对象——直到程序调用 image.show() 方法时,程序须要真正调用 BigImage 对象的 show() 方法,程序此时才真正建立 BigImage 对象。运行上面程序,看到如图 6 所示的结果。

图 6. 使用代理模式提升性能
图 6. 使用代理模式提升性能

看 到如图 6 所示的运行结果,读者应该能认同:使用代理模式提升了获取 Image 对象的系统性能。但可能有读者会提出疑问:程序调用 ImageProxy 对象的 show() 方法时同样须要建立 BigImage 对象啊,系统开销并未真正减小啊?只是这种系统开销延迟了而已啊?

咱们能够从以下两个角度来回答这个问题:

  • 把建立 BigImage 推迟到真正须要它时才建立,这样能保证前面程序运行的流畅性,并且能减小 BigImage 在内存中的存活时间,从宏观上节省了系统的内存开销。
  • 有些状况下,也许程序永远不会真正调用 ImageProxy 对象的 show() 方法——意味着系统根本无须建立 BigImage 对象。在这种情形下,使用代理模式能够显著地提升系统运行性能。

与此彻底相似的是,Hibernate 也是经过代理模式来“推迟”加载关联实体的时间,若是程序并不须要访问关联实体,那程序就不会去抓取关联实体了,这样既能够节省系统的内存开销,也能够缩短 Hibernate 加载实体的时间。

小结

Hibernate 的延迟加载(lazy load)本质上就是代理模式的应用,咱们在过去的岁月里就常常经过代理模式来下降系统的内存开销、提高应用的运行性能。Hibernate 充分利用了代理模式的这种优点,并结合了 Javassist 或 CGLIB 来动态地生成代理对象,这更加增长了代理模式的灵活性,Hibernate 给这种用法一个新名称:延迟加载。不管怎样,充分分析、了解这些开源框架的实现能够更好的感觉经典设计模式的优点所在。

参考资料

学习

  • 李刚编著 《轻量级 Java EE 企业应用实战 ( 第三版 ) 》,本书全面介绍了 Struts 2.二、Spring 3.0、Hibernate 3.6 整合开发的知识,并经过实际案例介绍了 3 大框架在实际项目中应用。
  • HIBERNATE - Relational Persistence for Idiomatic Java,全面介绍 Hibernate 框架功能和用法的参考手册。
  • Hibernate JavaDoc:介绍 Hibernate 3.6 全部 API 功能和用法的参考文档。
  • 评论专栏:真的,为何要用 Hibernate ?” (developerWorks,2010 年 10 月):IBM WebSphere Application Server 提供一个基于 Apache OpenJPA 项目的完整 JPA 解决方案。尽管使用其余 JPA 提供者(好比 Hibernate JPA)也是可行的,但问题是:“为何要选择 Hibernate JPA ?”本文解释为什么使用 WebSphere JPA 解决方案老是最明智的选择。
  • 针对 SaaS 应用程序的基于 Hibernate 框架的数据库分片”(developerWorks,2010 年 11 月):扩展共享数据库的一个最简单的技术是基于租户 ID 经过数据分片启用多租户数据架构。在本文中,咱们探究如何使用 Hibernate 分片来解决 SaaS 应用程序的数据库伸缩问题。
  • Struts二、Spring、Hibernate 高效开发的最佳实践” (developerWorks,2011 年 8 月):Struts二、Spring、Hibernate(SSH)是最经常使用的 Java EE Web 组件层的开发技术搭配,网络中和许多 IT 技术书籍中都有它们的开发教程,可是一般的教程都会让不少程序员陷入痛苦的配置与修改配置的过程。本文利用 SSH 中的技术特性,利用 Java 反射技术,按照规约优于配置的原理,基于 SSH 设定编写了一个通用开发框架,这使得开发者能够专一于业务逻辑的开发,而不用随着业务增长而添加或修改任何配置,而且对于权限控制和日志记录也提供了方便 的接口。
  • Hibernate 3 深度解析” (developerWorks,2011 年 8 月):Hibernate 的重要性,我想各位必定很清楚 . 那么 Hibernate 的一些重要功能项你们是否真的很了解呢,是否明白它的执行逻辑呢?本文章将会带领你们深刻到 Hibernate 的内部,深度解析下重要功能项或是关键属性的工做原理。
  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
相关文章
相关标签/搜索