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
2 對象加載<class>
2.1 延遲加載策略(默認)
如果想對實體對象使用延遲加載,必須要在實體的映射配置文件中進行相應的配置
<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()方法纔會返回實體類的代理類對象。
2.2 非延遲加載策略
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並不能控制關聯或集合的加載策略。
2.3 總結
Hibernate中<class lazy=””>默認爲true。如果,在load的時候只會返回一個代理類,並不會正在從數據庫中讀取數據。第一次用到時,會將所有普通屬性(set這種就不是)全部加載進來。如果第一次使用到時,session已經關閉將發生錯誤。
如果顯式是設置lazy=false,load的時候即會把所有普通屬性全部讀取進來。而且,返回的將是一個真正的該類型的對象(如Person),而不是代理類。
3 字段加載(property)
在Hibernate3中,引入了一種新的特性——屬性的延遲加載,這個機制又爲獲取高性能查詢提供了有力的工具。在大數據對象讀取時,如Person對象中有一個School字段,該字段是一個java.sql.Clob類型,包含了用戶的簡歷信息,當我們加載該對象時,我們不得不每一次都要加載這個字段,而不論我們是否真的需要它,而且這種大數據對象的讀取本身會帶來很大的性能開銷。
3.1 延遲加載
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.
4 集合無關聯
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>
4.1 非延遲加載策略
映射文件的配置<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=?
4.2 延遲加載策略
映射文件的配置<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=?
4.2 延遲加載extra
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=?
4.4 總結
在集合的3中延遲加載中,我覺得最有的配置應該是extra。但是,默認配置false和extra均不適用於,session會話之外的情況。
Hibernate中集合屬性的延遲加載應該來說是最爲重要的,因爲如果集合屬性裏面包含十萬百萬記錄,在初始化持久實體的同時,完成所有集合屬性的抓取,將導致性能急劇下降。
5 集合有關聯
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>
5.1 非延遲加載
映射文件配置<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=?
5.2 延遲加載與extra策略
與無關聯關係時一致,不再累述。
6 1-1和N-1延遲加載策略
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>
6.1 非延遲加載
映射文件配置<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類型對象。
6.2 延遲加載proxy
映射文件設置<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屬性便是個代理類,如下圖所示:
6.3 總結
默認情況下,Hibernate 也會採用延遲加載來加載關聯實體,不管是一對多關聯、還是一對一關聯、多對多關聯,Hibernate 默認都會採用延遲加載。
對於關聯實體,可以將其分爲兩種情況:
關聯實體是多個實體時(包括一對多、多對多):此時關聯實體將以集合的形式存在,Hibernate 將使用 PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 等集合來管理延遲加載的實體。這就是前面所介紹的情形。
關聯實體是單個實體時(包括一對一、多對一):當 Hibernate 加載某個實體時,延遲的關聯實體將是一個動態生成代理對象。
當關聯實體是單個實體時,也就是使用 <many-to-one.../> 或 <one-to-one.../> 映射關聯實體的情形,這兩個元素也可通過 lazy 屬性來指定延遲加載。
7 繼承(subclass爲例)
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>
8.1 非延遲加載
映射文件配置<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'
7.2 延遲加載
映射文件配置<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語句如上。
7.3 總結
繼承方式的延遲加載,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/