Hibernate Lazy属性

延迟加载策略简介

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

但是,延迟加载也是项目开发中特别常见的一个错误。如果对一个类或者集合配置了延迟检索策略,那么必须当代理类实例或代理集合处于持久化状态(即处于Session范围内)时,才能初始化它。如果在游离状态时才初始化它,就会产生延迟初始化错误。所以,在开发独立的DAO数据访问层时应该格外小心这个问题。

 

如果在获取对象的时候使用的是session.get()是不会延迟加载的,只有在使用load、hql时候才会延迟加载。

session.get()方法:

无论是否在class级别上设置了延迟加载,在调用get方法时,Hibernate都会直接去查询数据库,并加载所有的普通属性,然后返回一个真正的person对象。

对于关联对象集合属性,Hibernate会根据为关联对象集合属性设置的加载策略,来决定是否延迟加载

session.load()方法:

如果在类的级别上设置了延迟加载,在使用load方法加载对象时,Hibernate并不会去查询数据库,而是直接返回一个代理对象,这个代理对象只有id属性有值,其余的属性全部为空。当第一次使用代理对象的普通属性时,Hibernate会从数据库中查询出所有的普通属性无论普通属性的加载策略是什么);同时根据集合属性关联对象的加载策略来决定是否加载集合属性关联对象从下面的代码可以产出这一点:

下面是Person.hbm.xml的部分内容:

 

<!--在class的级别上设置了延迟加载,这会使hibernate在load对象时不会去查询数据库,而是返回一个代理对象-->
<class name="Person" table="person" lazy="true">
        <id name="id" type="int" column="id">
        	<generator class="identity"/>
        </id>
        <!--name和age为普通属性,都设置为延迟加载-->
        <property name="name" type="string" column="name" lazy="true"/>
        <property name="age" type="integer" column="age" lazy="true"/>
        <!--mainAddress为管理对象,设置为延迟加载-->
        <many-to-one name="mainAddress" class="Address" column="address_id" unique="true" cascade="all" lazy="proxy"/>
        <!--addressre为集合对象,设置为延迟加载-->
        <set name="addresses" inverse="true" cascade="all" lazy="true">
        	<key column="person_id" unique="false"/>
        	<one-to-many class="Address"/>
        </set>
</class>

 下面是查询person对象的代码:

 

Session session = HibernateUtil.openSession();
//执行下面的一行代码时,hibernate没有在控制台输出SQL语句,说明hibernate没有去查询数据库
//而是直接返回了一个代理对象,该代理对象只有id属性有值,其余的属性全部为空
Person p = (Person) session.load(Person.class, 2);
//执行下面的一行代码时,也没有输出SQL语句,说明id属性的值来自代理对象
System.out.println(p.getId());
// 第一次使用普通属性时,Hibernate会把所有的普通属性全部加载进来(无论这些普通属性的加载策略是什么)
// 但是Hibernate会根据关联属性和集合属性的加载策略来决定是这时(即加载普通属性时)加载,还是等到使用时再加载(即延迟加载)
//执行下面的一行代码时,可以从控制台显示的SQL语句看出,hibernate把person对象的全部普通属性都查询了出来
System.out.println(p.getName());
//执行下面的一行代码时,没有输出SQL语句却可以输出age的值,这是因为hibernate在执行上一条语句时已经把age的值也查询出来了
System.out.println(p.getAge());
//执行下面的一行代码时,输出了查询mainAddress的SQL语句,这说明关联对象的加载策略生效了
System.out.println(p.getMainAddress().getCity());
//执行下面的一行代码时,输出了查询addresses的SQL语句,这说明集合属性的记载策略也生效了
for (Address a : p.getAddresses()) {
	System.out.println(a.getCountry() + a.getProvince() + a.getCity());
}
HibernateUtil.closeSession();

 下面是在控制台输出的内容:

2
select person0_.id as id1_13_0_, person0_.name as name2_13_0_, person0_.age as age3_13_0_, person0_.address_id as address_4_13_0_ from person person0_ where person0_.id=?
李刚
20
select address0_.id as id1_0_0_, address0_.country as country2_0_0_, address0_.province as province3_0_0_, address0_.city as city4_0_0_, address0_.person_id as person_i5_0_0_ from address address0_ where address0_.id=?
济南市
select addresses0_.person_id as person_i5_13_1_, addresses0_.id as id1_0_1_, addresses0_.id as id1_0_0_, addresses0_.country as country2_0_0_, addresses0_.province as province3_0_0_, addresses0_.city as city4_0_0_, addresses0_.person_id as person_i5_0_0_ from address addresses0_ where addresses0_.person_id=?
中国山东省日照市
中国山东省东营市

 

如果没有在class的级别上设置延迟加载,在使用load方法加载对象时,Hibernate会直接去查询数据库,返回一个真正的person对象(而不是一个代理对象),同时加载id全部的普通属性。但是会根据关联对象集合对象的加载策略来决定是否加载关联对象集合对象(即如果关联对象集合属性设置为延迟加载,那么只有在使用它们时才回去加载;否则在hibernate加载普通属性时就会预先加载它们)。从下面的代码可以看出这一点:

Person.hbm.xml

<!--在类的级别设置为非延迟加载-->
<class name="Person" table="person" lazy="false">
        <id name="id" type="int" column="id">
        	<generator class="identity"/>
        </id>
        <!--name和age属性为普通属性,虽然设置为延迟加载,但hibernate依然回预先加载-->
        <property name="name" type="string" column="name" lazy="true"/>
        <property name="age" type="integer" column="age" lazy="true"/>
        <!--mainAddress为关联对象,设置为延迟加载,hibernate会延迟加载它-->
        <many-to-one name="mainAddress" class="Address" column="address_id" unique="true" cascade="all" lazy="proxy"/>
        <!--addresses为集合属性,也设置为延迟加载,hibernate会延迟加载它-->
        <set name="addresses" inverse="true" cascade="all" lazy="true">
        	<key column="person_id" unique="false"/>
        	<one-to-many class="Address"/>
        </set>
</class>

 下面是查询person对象的代码:

 

Session session = HibernateUtil.openSession();
//执行下面一行代码时,控制台会输出一条查询person对象的SQL语句,这就是在类的级别设置不延迟加载的效果,这条SQL语句将查询出id和全部的普通属性
Person p = (Person) session.load(Person.class, 2);
//因为id、name、age已在上面一条代码执行时全部查处,所以可以直接输出
System.out.println(p.getId());
System.out.println(p.getName());
System.out.println(p.getAge());
//因为mainAddress设置了延迟加载,所以只有在使用它时才会去加载它
//控制台会输出一条查询mainAddress的SQL语句
System.out.println(p.getMainAddress().getCity());
//因为addresses设置了延迟加载,所以只有在使用它时才会去加载它
//控制台会输出一条查询addresses的SQL语句
for (Address a : p.getAddresses()) {
	System.out.println(a.getCountry() + a.getProvince() + a.getCity());
}
HibernateUtil.closeSession();

 下面是在控制台输出的内容:

 

select person0_.id as id1_13_0_, person0_.name as name2_13_0_, person0_.age as age3_13_0_, person0_.address_id as address_4_13_0_ from person person0_ where person0_.id=?
2
李刚
20
 select address0_.id as id1_0_1_, address0_.country as country2_0_1_, address0_.province as province3_0_1_, address0_.city as city4_0_1_, address0_.person_id as person_i5_0_1_, person1_.id as id1_13_0_, person1_.name as name2_13_0_, person1_.age as age3_13_0_, person1_.address_id as address_4_13_0_ from address address0_ left outer join person person1_ on address0_.person_id=person1_.id where address0_.id=?
济南市
select addresses0_.person_id as person_i5_13_1_, addresses0_.id as id1_0_1_, addresses0_.id as id1_0_0_, addresses0_.country as country2_0_0_, addresses0_.province as province3_0_0_, addresses0_.city as city4_0_0_, addresses0_.person_id as person_i5_0_0_ from address addresses0_ where addresses0_.person_id=?
中国山东省日照市
中国山东省东营市

 

 

 

对象的id属性是一定预先加载的,因为后续的查询需要根据id来查询。

 

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

<list lazy=(true|extra|false)>:默认为true

<set lazy=(true|extra|false)>:默认为true

<map lazy=(true|extra|false)>:默认为true

<bag lazy=(true|extra|false)>:默认为true

<idbag lazy=(true|extra|false)>:默认为true

 

1、对象的延迟加载(<class/>元素)分析     true或false

1.1 延迟加载     lazy=true

如果想对实体对象使用延迟加载,必须要在实体的映射配置文件中进行相应的配置

如果在class的级别设置了延迟加载,那么在用session.load(Person.class, 1);加载对象时,Hibernate不会去查询数据库(因为在控制台根本就不会输出SQL语句),而是直接返回了一个代理对象,这个代理对象只有id属性有值,其余的属性全部为空。只有当使用到除id以外的的属性时,hibernate才会去查询数据库。如果在使用除id以外的属性时session已经关闭,那么Hibernate就会抛出异常。

 

<class name= "Person" table= "PERSON" lazy= "true">
</class>

 

tx = session.beginTransaction();   
Person p=(Person) session.load(Person.class, "001");//(1)   
System.out.println("0: "+p.getPersonId());//(2)   
System.out.println("0: "+p.getName());//(3)   
tx.commit();   
session.close();

 

执行到(1)并没有出现sql语句,并没有从数据库中抓取数据。这个时候查看内存对象p如下:


 观察person对象,我们可发现是Person$$EnhancerBy..的类型的对象。这里所返回的对象类型就是Person对象的代理对象,在hibernate中通过使用CGLB来先动态构造一个目标对象的代理类对象,并且在代理对象中包含目标对象的所有属性和方法。所以,对于客户端而言是否为代理类是无关紧要的,对他来说是透明的。这个对象中,仅仅设置了id属性(即personId的值),这是为了便于后面根据这个Id从数据库中来获取数据。

 

运行到(2)处,输出为001,但是仍然没有从数据库里面读取数据。这个时候代理类的作用就体现出来了,客户端觉得person类已经实现了(事实上并未创建)。但是,如果这个会话session关闭,再使用person对象就会出错了。

 

调试运行到(3)处,要用到name属性,但是这个值在数据库中。所以hibernate从数据库里面抓取了数据,sql语句如下所示:

select   
   person0_.PERSONID as PERSONID3_0_,   
   person0_.NAME as NAME3_0_    
from   
   PERSON person0_    
where   
   person0_.PERSONID=?

 这时候,我们查看内存里面的对象如下:


真正的Person对象放在CGLIB$CALLBACK_0对象中的target属性里。

 

这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。

 

1.2  非延迟加载策略分析     lazy=false

Hibernate默认的策略便是非延迟加载的,所以设置lazy=false。

如果没有在class的级别上设置延迟加载,在使用load方法加载对象时,Hibernate会直接去查询数据库,返回一个真正的person对象(而不是一个代理对象),同时加载id全部的普通属性。但是会根据关联对象集合对象的加载策略来决定是否加载关联对象集合对象(即如果关联对象集合属性设置为延迟加载,那么只有在使用它们时才回去加载;否则在hibernate加载普通属性时就会预先加载它们)。

 

tx = session.beginTransaction();   
Person p=(Person) session.load(Person.class, "001");//(1)   
System.out.println("0: "+p.getPersonId());//(2)   
System.out.println("0: "+p.getName());//(3)   
tx.commit();   
session.close();

 调试运行到(1)处时,hibernate直接执行如下sql语句:

 

select   
    person0_.PERSONID as PERSONID3_0_,   
    person0_.NAME as NAME3_0_    
from   
    PERSON person0_    
where   
    person0_.PERSONID=?

 我们在查看内存快照如下:


 

这个时候就不是一个代理类了,而是Person对象本身了。里面的属性也已经全部 普通属性也全部被加载。这里说普通属性是因为addresses这个集合对象并没有被加载,因为set自己本身也可以设置lazy属性。所以,这里也反映出class对象的lazy并不能控制关联或集合的加载策略。

 

1.3  总结

如果设置lazy="true"(默认为true)。在load的时候Hibernate并没有到数据库中查询,而是仅仅返回了一个代理对象,该代理对象只有id属性有值,其余的属性全部为空,当第一次使用代理对象的普通属性时,Hibernate会从数据库中查询出全部的普通属性,关联对象集合属性会根据它自己的加载策略来决定是在这时(即加载普通属性时)加载,还是在之后的第一次使用时加载。

 

如果显式的设置lazy="false",load的时候即会把所有 普通属性全部读取进来(无论它们的加载策略是什么),而关联对象和 集合属性 将根据自己的加载策略决定是否延迟加载。并且,返回的将是一个真正的该类型的对象(如Person),而不是代理类。

 

2、属性的延迟加载(<property/>元素)分析     true或false

<property/>元素也有一个名为lazy的属性,从理论上来讲该属性可以用来控制单个属性的加载策略。但实际上,该属性并不能控制单个属性的加载策略。

属性的加载策略会受到<class/>元素加载策略的影响。

具体情况请参见  “1.3总结”  。

如果只对部分property进行延迟加载的话,hibernate提供了另外的方式,也是更为推荐的方式,即HQL或者条件查询。

 

3、集合属性的延迟加载(<set/>等集合元素)分析     true、false或extra

true或false:

集合元素加载策略不受<class/>元素加载策略的影响,即无论<class/>元素的加载策略是什么,集合元素均按照它自己的加载策略来决定是否延迟加载。

 

extra:

extra其实是一种比较智能的延迟加载,即调用集合的size/contains等方法的时候,hibernate并不会去加载整个集合的数据,而是发出一条聪明的SQL语句,以便获得需要的值,只有在真正需要用到这些集合元素对象数据的时候,才去发出查询语句加载所有对象的数据。

public String getListData() {
	Session session = HibernateUtil.openSession();
	ListData ld = (ListData) session.load(ListData.class, 1);
	// HibernateUtil.closeSession();
	System.out.println(ld.getId());
	System.out.println("intList.length=" + ld.getIntList().size());
	List<Integer> intList = ld.getIntList();
	for (Integer num : intList)
		System.out.print(num + "  ");
	System.out.println();
	System.out.println("strList.length=" + ld.getStrList().size());
	List<String> strList = ld.getStrList();
	for (String str : strList)
		System.out.print(str + "  ");
	System.out.println();
	System.out.println("dateList.length=" + ld.getDateList().size());
	List<Date> dateList = ld.getDateList();
	for (Date date : dateList)
		System.out.print(date + "  ");
	System.out.println();
	HibernateUtil.closeSession();
	return SUCCESS;
}

 Hibernate生成的SQL语句如下:

select listdata0_.id as id1_11_0_ from list_data listdata0_ where listdata0_.id=?
select max(list_index) + 1 from int_list where foreign_id =?
select 
    intlist0_.foreign_id as foreign_1_11_0_, 
    intlist0_.number as number2_7_0_, 
    intlist0_.list_index as list_ind3_0_ 
from 
    int_list intlist0_ 
where 
    intlist0_.foreign_id=?

select max(list_index) + 1 from str_list where foreign_id =?
select 
    strlist0_.foreign_id as foreign_1_11_0_, 
    strlist0_.str as str2_16_0_, 
    strlist0_.list_index as list_ind3_0_ 
from 
    str_list strlist0_ 
where 
    strlist0_.foreign_id=?

select max(list_index) + 1 from date_list where foreign_id =?
select 
    datelist0_.foreign_id as foreign_1_11_0_, 
    datelist0_.date_time as date_tim2_3_0_, 
    datelist0_.list_index as list_ind3_0_ 
from 
    date_list datelist0_ 
where
    datelist0_.foreign_id=?

 总结

在集合的3中延迟加载中,我觉得最有的配置应该是extra。

Hibernate中集合属性的延迟加载应该来说是最为重要的,因为如果集合属性里面包含十万百万记录,在初始化持久实体的同时,完成所有集合属性的抓取,将导致性能急剧下降。