Hibernate 的延迟加载(lazy load)是一个被广泛使用的技术。这种延迟加载保证了应用只有在需要时才去数据库中抓取相应的记录。通过延迟加载技术可以避免过多、过早地加载数据表里的数据,从而降低应用的内存开销。Hibernate 的延迟加载本质上就是代理模式的应用,当程序通过 Hibernate 装载一个实体时,默认情况下,Hibernate 并不会立即抓取它的集合属性、关联实体所以对应的记录,而是通过生成一个代理来表示这些集合属性、关联实体,这就是代理模式应用带来的优势。
但是,延迟加载也是项目开发中特别常见的一个错误。如果对一个类或者集合配置了延迟检索策略,那么必须当代理类实例或代理集合处于持久化状态(即处于Session范围内)时,才能初始化它。如果在游离状态时才初始化它,就会产生延迟初始化错误。所以,在开发独立的DAO数据访问层时应该格外小心这个问题。
如果在获取对象的时候使用的是session.get()是不会延迟加载的,只有在使用load、hql时候才会延迟加载。
Hibernate中允许使用延迟加载的地方主要有以下几个地方:
<hibernate-mapping default-lazy=(true|false)”true”>:设置全局的延迟加载策略。
<class lazy=(true|false)>:DTD没设置默认值,推理默认值为true
<property lazy=(true|false)>:设置字段延迟加载,默认为false
<component lazy=(true|false):默认为false
<subclass lazy=(true|false)>:默认设置为true
<join-subclass lazy=(true|false)>:默认设置为true
<union-subclass lazy=(true|false)>:默认设置为true
<many-to-one lazy=(proxy|no-proxy|false)>:默认为proxy
<one-to-one lazy=(proxy|no-proxy|false)>:默认为proxy
<map lazy=(true|extra|false)>:默认为true
<set lazy=(true|extra|false)>:默认为true
<bag lazy=(true|extra|false)>:默认为true
<ibag lazy=(true|extra|false)>:默认为true
<list lazy=(true|extra|false)>:默认为true
如果想对实体对象使用延迟加载,必须要在实体的映射配置文件中进行相应的配置
<class name="Person" table="PERSON" lazy="true">
1 tx = session.beginTransaction(); 2 Person p=(Person) session.load(Person.class, "001");//(1) 3 System.out.println("0: "+p.getPersonId());//(2) 4 System.out.println("0: "+p.getName());//(3) 5 tx.commit(); 6 session.close();
执行到(1)并没有出现sql语句,并没有从数据库中抓取数据。这个时候查看内存对象p如下:
图2.1 person对象load时的内存快照
观察person对象,我们可发现是Person$$EnhancerBy..的类型的对象。这里所返回的对象类型就是Person对象的代理对象,在hibernate中通过使用CGLB来先动态构造一个目标对象的代理类对象,并且在代理对象中包含目标对象的所有属性和方法。所以,对于客户端而言是否为代理类是无关紧要的,对他来说是透明的。这个对象中,仅仅设置了id属性(即personId的值),这是为了便于后面根据这个Id从数据库中来获取数据。
运行到(2)处,输出为001,但是仍然没有从数据库里面读取数据。这个时候代理类的作用就体现出来了,客户端觉得person类已经实现了(事实上并未创建)。但是,如果这个会后session关闭,再使用person对象就会出错了。
调试运行到(3)处,要用到name属性,但是这个值在数据库中。所以hibernate从数据库里面抓取了数据,sql语句如下所示:
Hibernate: select person0_.PERSONID as PERSONID3_0_, person0_.NAME as NAME3_0_ from PERSON person0_ where person0_.PERSONID=?
这时候,我们查看内存里面的对象如下:
图2.2 class延迟加载时内存对象
真正的Person对象放在CGLIB$CALLBACK_0对象中的target属性里。
这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。
Hibernate默认的策略便是非延迟加载的,所以设置lazy=false
1 tx = session.beginTransaction(); 2 Person p=(Person) session.load(Person.class, "001");//(1) 3 System.out.println("0: "+p.getPersonId());//(2) 4 System.out.println("0: "+p.getName());//(3) 5 tx.commit(); 6 session.close();
调试运行到(1)处时,hibernate直接执行如下sql语句:
Hibernate: select person0_.PERSONID as PERSONID3_0_, person0_.NAME as NAME3_0_ from PERSON person0_ where person0_.PERSONID=?
我们在查看内存快照如下:
这个时候就不是一个代理类了,而是Person对象本身了。里面的属性也已经全部普通属性也全部被加载。这里说普通属性是因为addresses这个集合对象并没有被加载,因为set自己本身也可以设置lazy属性。所以,这里也反映出class对象的lazy并不能控制关联或集合的加载策略。
Hibernate中<class lazy=””>默认为true。如果,在load的时候只会返回一个代理类,并不会正在从数据库中读取数据。第一次用到时,会将所有普通属性(set这种就不是)全部加载进来。如果第一次使用到时,session已经关闭将发生错误。
如果显式是设置lazy=false,load的时候即会把所有普通属性全部读取进来。而且,返回的将是一个真正的该类型的对象(如Person),而不是代理类。
在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在大数据对象读取时,如Person对象中有一个School字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。
1、 <class lazy=”false”>
配置如下
1 tx = session.beginTransaction(); 2 Person p=(Person) session.load(Person.class, "001");//(1) 3 System.out.println("");//(2) 4 System.out.println("0: "+p.getPersonId());//(3) 5 System.out.println("0: "+p.getName());//(4) 6 System.out.println("0: "+p.getSchool());//(5) 7 tx.commit();
1 <property name="name" type="java.lang.String"> 2 <column name="NAME" /> 3 </property> 4 <property name="school" type="java.lang.String" lazy="true"> 5 <column name="SCHOOL"></column> 6 </property>
当运行到p的时候,全部加载了,执行语句如下:
Hibernate: select person0_.PERSONID as PERSONID3_0_, person0_.NAME as NAME3_0_, person0_.SCHOOL as SCHOOL3_0_ from PERSON person0_ where person0_.PERSONID=?
所有普通属性都均已加载。
2、<class lazy=”true”>
School的lazy属性自然还是true。当程序运行到(4)时,也同样加载了全部属性,执行了如下sql:
Hibernate: select person0_.PERSONID as PERSONID3_0_, person0_.NAME as NAME3_0_, person0_.SCHOOL as SCHOOL3_0_ from PERSON person0_ where person0_.PERSONID=?
结果就是无效,不管采用何种策略都是无效的,和我们想想的有较大出路。下面是一段来自hibernate官方文档的话。
Lazy property loading requires buildtime bytecode instrumentation. If your persistent classes are not enhanced, Hibernate will ignore lazy property settings and return to immediate fetching.
应该是因为,我们并未用到编译时字节码增强技术的原因。如果只对部分property进行延迟加载的话,hibernate还提供了另外的方式,也是更为推荐的方式,即HQL或者条件查询。
A different way of avoiding unnecessary column reads, at least for read-only transactions, is to use the projection features of HQL or Criteria queries. This avoids the need for buildtime bytecode processing and is certainly a preferred solution.
Person类
1 public class Person { 2 private String name; 3 private String sex; 4 private Set<String> addresses; 5 }
Person.hbm.xml
1 <class name="com.hbm.hibernate.Person" table="PERSON"> 2 <id name="name" type="java.lang.String"> 3 <column name="NAME"/> 4 <generator class="assigned"/> 5 </id> 6 <property name="sex" type="java.lang.String"> 7 <column name="SEX"/> 8 </property> 9 <set name="addresses" table="ADDRESSES" inverse="false" lazy="true" fetch="join"> 10 <key column="NAME"/> 11 <element column="ADDRESS" type="java.lang.String"></element> 12 </set> 13 </class>
映射文件的配置<set lazy=”false”>。
1 tx = session.beginTransaction(); 2 Person person=(Person) session.load(Person.class, "XiJinping");//(1) 3 System.out.println("");//(2) 4 System.out.println("0: "+person.getName());//(3) 5 System.out.println("1: "+person.getSex());//(4) 6 System.out.println("2: "+person.getAddresses());//(5) 7 tx.commit();
运行到(4)处时,加载了全部属性,执行了如下sql语句。
1 Hibernate: 2 /* load com.hbm.hibernate.Person */ select 3 person0_.NAME as NAME0_0_, 4 person0_.SEX as SEX0_0_ 5 from 6 PERSON person0_ 7 where 8 person0_.NAME=? 9 Hibernate: 10 /* load collection com.hbm.hibernate.Person.addresses */ select 11 addresses0_.NAME as NAME0_, 12 addresses0_.ADDRESS as ADDRESS0_ 13 from 14 ADDRESSES addresses0_ 15 where 16 addresses0_.NAME=?
fetch策略的配合使用,当<set lazy=”false” fetch=”join”>时,执行的sql语句如下。这个是有,将不再采用两条select语句的方式,而是采用左连接的方式进行,有利于提高效率。
Hibernate: /* load com.hbm.hibernate.Person */ select person0_.NAME as NAME0_0_, person0_.SEX as SEX0_0_, addresses1_.NAME as NAME2_, addresses1_.ADDRESS as ADDRESS2_ from PERSON person0_ left outer join ADDRESSES addresses1_ on person0_.NAME=addresses1_.NAME where person0_.NAME=?
映射文件的配置<set lazy=”true”>。
当程序运行到(4),hibernate加载了Person对象的其他全部属性,执行了如下sql语句。
Hibernate: /* load com.hbm.hibernate.Person */ select person0_.NAME as NAME0_0_, person0_.SEX as SEX0_0_ from PERSON person0_ where person0_.NAME=?
当程序运行到(5)时,hibernate加载了所有的address对象,执行如下sql语句。
1 Hibernate: 2 /* load collection com.hbm.hibernate.Person.addresses */ select 3 addresses0_.NAME as NAME0_, 4 addresses0_.ADDRESS as ADDRESS0_ 5 from 6 ADDRESSES addresses0_ 7 where 8 addresses0_.NAME=?
It can also be used to enable "extra-lazy" fetching where most operations do not initialize the collection. This is suitable for large collections.
大部分操作的时候并不会加载集合,适用于大的集合。extra其实是一种比较智能的延迟加载,即调用集合的size/contains等方法的时候,hibernate并不会去加载整个集合的数据,而是发出一条聪明的SQL语句,以便获得需要的值,只有在真正需要用到这些集合元素对象数据的时候,才去发出查询语句加载所有对象的数据。
映射文件配置映射文件的配置<set lazy=”extra”>
1 public int getNum(){ 2 return addresses.size(); 3 } 4 5 tx = session.beginTransaction(); 6 Person person=(Person) session.load(Person.class, "XiJinping");//(1) 7 System.out.println("");//(2) 8 System.out.println("0: "+person.getName());//(3) 9 System.out.println("1: "+person.getSex());//(4) 10 System.out.println("2: "+person.getNum());//(5) 11 System.out.println("3: "+person.getAddresses());//(6) 12 tx.commit();
当程序运行到(4)时,进行了第一次的加载,加载了person对象的所有普通属性,执行sql如下:
Hibernate: /* load com.hbm.hibernate.Person */ select person0_.NAME as NAME0_0_, person0_.SEX as SEX0_0_ from PERSON person0_ where person0_.NAME=?
当程序运行到(5)时,进行了第二次加载,这个时候并没有去加载set集合中的所有属性,hibernate智能的用sql语句获取了集合中的数量,执行的sql语句如下:
Hibernate: select count(ADDRESS) from ADDRESSES where NAME =?
当程序运行到(6)时,进行了第三次加载,将集合中的所有对象均加载进来了,执行的sql语句如下:
Hibernate: /* load collection com.hbm.hibernate.Person.addresses */ select addresses0_.NAME as NAME0_, addresses0_.ADDRESS as ADDRESS0_ from ADDRESSES addresses0_ where addresses0_.NAME=?
在集合的3中延迟加载中,我觉得最有的配置应该是extra。但是,默认配置false和extra均不适用于,session会话之外的情况。
Hibernate中集合属性的延迟加载应该来说是最为重要的,因为如果集合属性里面包含十万百万记录,在初始化持久实体的同时,完成所有集合属性的抓取,将导致性能急剧下降。
Person类
1 public class Person { 2 private String personId; 3 private String name; 4 private Set addresses; 5 public int getNum(){ 6 return addresses.size(); 7 } 8 }
Address类
1 public class Address { 2 private String addressId; 3 private String addressDetail; 4 private Set people; 5 }
Person.hbm.xml
<class name="com.hbm.hibernate.Person" table="PERSON"> ……… <set name="addresses" table="PERSON_ADDRESS" cascade="all"> <key> <column name="PERSONID" /> </key> <many-to-many class="com.hbm.hibernate.Address" column="ADDRESSID"></many-to-many> </set> </class>
映射文件配置<set lazy=”false”>
1 tx = session.beginTransaction(); 2 Person person=(Person) session.load(Person.class, "001");//(1) 3 System.out.println("");//(2) 4 System.out.println("0: "+person.getPersonId());//(3) 5 System.out.println("1: "+person.getName());//(4) 6 System.out.println("2: "+person.getNum());//(5) 7 System.out.println("3: "+person.getAddresses());//(6) 8 tx.commit();
当程序运行到(4)时,hibernate加载了所有属性,执行的sql语句如下:
Hibernate: select person0_.PERSONID as PERSONID2_0_, person0_.NAME as NAME2_0_ from PERSON person0_ where person0_.PERSONID=? Hibernate: select addresses0_.PERSONID as PERSONID1_, addresses0_.ADDRESSID as ADDRESSID1_, address1_.ADDRESSID as ADDRESSID0_0_, address1_.ADDRESSDETAIL as ADDRESSD2_0_0_ from PERSON_ADDRESS addresses0_ left outer join ADDRESS address1_ on addresses0_.ADDRESSID=address1_.ADDRESSID where addresses0_.PERSONID=?
与无关联关系时一致,不再累述。
LineItem类
public class LineItem { private int lineNumber; private int amount; private double price; private Product product; }
Product类
public class Product { private String id; private String name; private double listprice; }
LineItem.hbm.xml
<class name="com.hbm.hibernate.LineItem" table="LINEITEM"> <id name="lineNumber" type="int"> <column name="LINENUMBER" /> <generator class="assigned" /> </id> <property name="amount" type="int"> <column name="AMOUNT" /> </property> <property name="price" type="double"> <column name="PRICE" /> </property> <join table="LINE_PRODUCT"> <key column="LINENUMBER"/> <many-to-one name="product" unique="true" lazy="false" not-null="true" column="PRODUCTID"/> </join> </class>
映射文件配置<many-to-one lazy=”false”>
1 tx = session.beginTransaction(); 2 LineItem l=(LineItem) session.load(LineItem.class, 2);//(1) 3 System.out.println("");//(2) 4 System.out.println("0: "+l.getLineNumber());//(3) 5 System.out.println("1: "+l.getAmount());//(4) 6 System.out.println("2: "+l.getProduct());//(5) 7 tx.commit();
程序运行到(4)处时,hibernate加载了所有属性,执行了如下sql语句:
Hibernate: select lineitem0_.LINENUMBER as LINENUMBER1_0_, lineitem0_.AMOUNT as AMOUNT1_0_, lineitem0_.PRICE as PRICE1_0_, lineitem0_1_.PRODUCTID as PRODUCTID2_0_ from LINEITEM lineitem0_ inner join LINE_PRODUCT lineitem0_1_ on lineitem0_.LINENUMBER=lineitem0_1_.LINENUMBER where lineitem0_.LINENUMBER=? Hibernate: select product0_.PRODUCTID as PRODUCTID0_0_, product0_.NAME as NAME0_0_, product0_.LISTPRICE as LISTPRICE0_0_ from PRODUCT product0_ where product0_.PRODUCTID=?
在这个时候,去查看内存中的LineItem类型对象,我们发现也是一个代理类。而回调函数中,tagert属性中的Prdouct是一个真正的Product类型对象。
映射文件设置<many-to-one lazy=”proxy”>
当程序运行到(4)时,进行了第一次的加载,执行的sql语句如下:
Hibernate: select lineitem0_.LINENUMBER as LINENUMBER1_0_, lineitem0_.AMOUNT as AMOUNT1_0_, lineitem0_.PRICE as PRICE1_0_, lineitem0_1_.PRODUCTID as PRODUCTID2_0_ from LINEITEM lineitem0_ inner join LINE_PRODUCT lineitem0_1_ on lineitem0_.LINENUMBER=lineitem0_1_.LINENUMBER where lineitem0_.LINENUMBER=?
当程序运行到(5)时,进行了第二次的加载,执行的sql语句如下:
Hibernate: select product0_.PRODUCTID as PRODUCTID0_0_, product0_.NAME as NAME0_0_, product0_.LISTPRICE as LISTPRICE0_0_ from PRODUCT product0_ where product0_.PRODUCTID=?
这个时候,我们去参看内存,发现target中的product属性便是个代理类,如下图所示:
默认情况下,Hibernate 也会采用延迟加载来加载关联实体,不管是一对多关联、还是一对一关联、多对多关联,Hibernate 默认都会采用延迟加载。
对于关联实体,可以将其分为两种情况:
关联实体是多个实体时(包括一对多、多对多):此时关联实体将以集合的形式存在,Hibernate 将使用 PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 等集合来管理延迟加载的实体。这就是前面所介绍的情形。
关联实体是单个实体时(包括一对一、多对一):当 Hibernate 加载某个实体时,延迟的关联实体将是一个动态生成代理对象。
当关联实体是单个实体时,也就是使用 <many-to-one.../> 或 <one-to-one.../> 映射关联实体的情形,这两个元素也可通过 lazy 属性来指定延迟加载。
Payment类
1 public class Payment { 2 private long id; 3 private long amount; 4 }
CreditCardPayment类
public class CreditCardPayment extends Payment { private long creditId; private String cardType; }
creditCardPayment.hbm.xml
<subclass name="com.hbm.hibernate.CreditCardPayment" discriminator-value="CREDIT" extends="com.hbm.hibernate.Payment" lazy="false"> <property name="creditId" column="CREDITID" type="long"></property> <property name="cardType" column="CARDTYPE" type="java.lang.String"></property> </subclass>
映射文件配置<subclass lazy=”false”>。
1 tx = session.beginTransaction(); 2 CreditCardPayment ccp=(CreditCardPayment) session.load(CreditCardPayment.class,new Long(8889));//(1) 3 System.out.println("");//(2) 4 System.out.println("0: "+ccp.getId());//(3) 5 System.out.println("1: "+ccp.getAmount());//(4) 6 System.out.println("2: "+ccp.getCardType());//(5) 7 tx.commit();
程序运行到(1)时,加载全部属性,执行的sql语句如下:
Hibernate: select creditcard0_.ID as ID0_0_, creditcard0_.AMOUNT as AMOUNT0_0_, creditcard0_.CREDITID as CREDITID0_0_, creditcard0_.CARDTYPE as CARDTYPE0_0_ from PAYMENT creditcard0_ where creditcard0_.ID=? and creditcard0_.PAYMENT_TYPE='CREDIT'
映射文件配置<subclass lazy=”true”>
Hibernate: select creditcard0_.ID as ID0_0_, creditcard0_.AMOUNT as AMOUNT0_0_, creditcard0_.CREDITID as CREDITID0_0_, creditcard0_.CARDTYPE as CARDTYPE0_0_ from PAYMENT creditcard0_ where creditcard0_.ID=? and creditcard0_.PAYMENT_TYPE='CREDIT'
程序执行到(4)时,第一次加载全部属性,执行的sql语句如上。
继承方式的延迟加载,set等在true或false时并未显著差别,在这里不再累述。
参考文献
1、http://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html_single/
2、http://blog.163.com/xi_zh_qi/blog/static/8501594200812695053939/
3、http://blog.csdn.net/linxinghui/article/details/3862324
4、http://www.ibm.com/developerworks/cn/java/j-lo-hibernatelazy/
原文:360doc