為了避免在某些情況下,關聯關係所帶來的無謂的效能開銷。
所謂消極式載入,就是在需要資料的時候,才真正執行資料載入操作。
Hibernate2中的消極式載入實現主要針對:
1. 實體物件。
2. 集合Collection)。
Hibernate3同時提供了屬性的消極式載入功能。
1. 實體物件的消極式載入
通過load方法可以指定可以返回目標實體物件的代理。
通過class的lazy屬性,可以開啟實體物件的消極式載入功能。對應檔)
(Hibernate2中,lazy預設為false;Hibernate3預設true)
非消極式載入的例子:
<hibernate-mapping> <class name="...TUser" table="t_user" dynamic-update="false" dynamic-insert="false" select-before-update="false" optimistic-lock="version" lazy="false" >...<hibernate-mapping>
TUser user = (Tuser)session.load(TUser.class, new Integer(1)); (1)System.out.println(user.getName()); (2)
當程式運行到1)時,Hibernate已經從庫表中取出了對應的記錄,並構造了一個完整的TUser對象。
對以上映射配置修改:
lazy=”true”
看代碼運行至1)後的user對象狀態(Eclipse Debug視圖)
可以看到,此時的user對象與我們之前定義的實體類並不相同,其當前類型描述為TUser$EnhancerByCGLIB$$bede8986,且其屬性均為null。
同時觀察螢幕日誌,此時並沒有任何Hibernate SQL輸出,也就意味著,當我們獲得user對象引用的時候,Hibernate並沒有執行資料庫查詢操作。
代碼運行至2),再次觀察user對象狀態
看到user對象的name屬性仍然是null,但是觀察螢幕輸出,看到查詢操作已經執行,同時user.name屬性也正確輸出。
兩次查詢操作為什麼會有這樣的差異?
原因就在於Hibernate的代理機制。
Hibernate中引入了CGLib作為代理機制實現的基礎。這也就是為什麼我們會獲得一個諸如TUser$EnhancerByCGLIB$$bede8986類型對象的緣由。
CGLib可以在運行期動態產生Java Class。這裡的代理機制,其基本實現原理就是通過由CGLib構造一個包含目標對象所有屬性和方法的動態對象相當於動態構造目標對象的一個子類)返回,並以之作為中介,為目標對象提供更多的特性。
從記憶體快照可以看到,真正的TUser對象位於代理類的CGLIB$CALLBACK_0.target屬性中。
當我們調用user.getName方法時,調用的實際上是CGLIB$CALLBACK_0.getName()方法,當方法調用後,它會首先檢查CGLIB$CALLBACK_0.target中是否存在目標對象。
如果存在,則調用目標對象的getName方法返回,如果目標對象為空白,則發起資料庫查詢指令,讀取記錄、構建目標對象並將其設入CGLIB$CALLBACK_0.target。
這樣,通過一個中間代理,實現了資料消極式載入功能,只有當客戶程式真正調用實體類的取值方法時,Hibernate才會執行資料庫查詢操作。
2. 集合類型的消極式載入
Hibernate消極式載入機制中,關於集合的消極式載入特性意義最為重大,也是實際應用中相當重要的一個環節。
如果我們只想要獲得user的年齡age)屬性,而不關心user的地址資訊(地址是集合類型),那麼自動載入address的特性就顯得特別多餘,並造成了極大的效能浪費。
將前面一對多關聯性中的lazy屬性修改為true,即指定了關聯對象採用消極式載入:
<hibernate-mapping> <class name="" table="" dynamic-update="" dynamic-insert=""> ... <set name="addresses" table="t_address" lazy="false"...>...
嘗試執行以下代碼:
Criteria criteria = session.createCriteria(TUser.class);criteria.add(Expression.eq("name","Erica"));List userList = criteria.list();Tuser user = (Tuser)userList.get(0);System.out.println("User Name=>"+user.getName());Set hset = user.getAddresses();session.close();//關閉sessionTaddress addr = (Taddress)hset.toArray()[0];System.out.println(addr.getAddress());
運行時拋出異常:
LazyInitializationException – failed to lazily initialize a collection – no session or session was closed
如果稍做調整,將session.close放在代碼末尾,則不會發生這樣的問題。
這意味著,只有我們實際載入user關聯的address時,Hibernate才試圖通過session從資料庫中載入實際的資料集,而由於我們讀取address之前已經關閉了session,所以出現了以上的錯誤。
這裡有個問題,如果我們採用了消極式載入機制,但希望在一些情況下實現非消極式載入時的功能,也就是說,希望在session關閉後,仍然允許操作user的address屬性。
Hibernate.initialize方法可以強制Hibernate立即載入關聯對象集:
Hibernate.initialize(user.getAddress());session.close();//通過Hibernate.initialize方法強制讀取資料//addresses對象即可脫離session進行操作Set hset = user.getAddresses();Taddress addr = (Taddress)hset.toArray()[0];System.out.println(addr.getAddress());
為了實現透明化的消極式載入機制,Hibernate進行了大量努力。其中包括JDK Collection介面的獨立實現。
如果嘗試用HashSet強行轉化Hibernate返回的Set型對象:
Set hset = (HashSet)user.getAddresses();
就會在運行期得到一個java.lang.ClassCastException,實際上,此時返回的是一個Hibernate的特定Set實現“net.sf.hibernate.collection.Set”, 而非傳統意義上的JDK Set實現。
這也正是為什麼在編寫POJO時,必須用JDK Collection Interface如Set,Map),而非特定的JDK Collection實作類別如HashSet, HashMap)聲明Colleciotn型屬性的原因如private Set addresses; 而非private HashSet addresses)。
當調用session.save(user);時,Hibernate如何處理其關聯的Addresses對象集?
假設TUser定義如下:
public class TUser implements Serializable{ … private Set addresses = new HashSet(); …}
我們通過Set介面,聲明了一個addresses屬性,並建立了一個HashSet作為addresses的初始執行個體,以便建立TUser執行個體後,就可以為其添加關聯的address對象:
TUser user = new TUser();TAddress addr = new TAddress();addr.setAddress(“HongKong”);user.getAddresses().add(addr);session.save(user);
通過Eclipse的Debug視圖,可以看到session.save方法執行前後user對象發生的變化:
首先,由於Insert操作,Hibernate獲得資料庫產生的id值,並填充到user對象的id屬性。
另一方面,Hibernate使用了自己的Collection實現”net.sf.hibernate.collection.Set”對user中的HashSet型addresses屬性進行了替換,並用資料對其進行填充,保證新的addresses與原有的addresses包含同樣的實體元素。
再來看下面的代碼:
TUser user = (TUser)session.load(TUser.class, new Integer(1));Collection addSet = user.getAddresses();(1)Iterator it = addSet.iterator();(2)while(it.hasNext()){ TAddress addr = (TAddress)it.next(); System.out.println(addr.getAddresses());}
當代碼執行到1)處時,addresses資料集尚未讀入,我們得到的addrSet對象實際上只是一個未包含任何資料的net.sf.hibernate.collection.Set執行個體。
代碼運行至2),真正的資料讀取操作才開始執行。
觀察一下net.sf.hibernate.collection.Set.iterator方法可以看到:
public Iterator iterator(){ read(); return new IteratorProxy(set.iterator());}
直到此時,真正的資料載入read方法)才開始執行。
read方法將首先在緩衝中尋找是否有合格資料索引。
這裡注意資料索引的概念,Hibernate在對集合類型進行緩衝時,分兩部分儲存,首先是這個集合中所有實體的id列表也就是所謂的資料索引,對於這裡的例子,資料索引中包含了所有userid=1的address對象的id清單),其次是各個實體物件。
如果沒有發現對應的資料索引】,則執行一條Select SQL對於本例就是select…from t_address where user_id=?)獲得所有合格記錄,接著構造實體物件和資料索引後返回。實體物件和資料索引也同時被分別納入緩衝。
如果發現了對應的資料索引】,則從這個資料索引中取出所有id列表,並根據id列表依次從緩衝中查詢對應的address對象,如果找到,則以緩衝中的資料返回,如果沒找到當前id對應的資料,則執行相應的Select SQL獲得對應的address記錄對於本例就是select…from t_address where user_id=?)。
這裡引出另一個效能關注點,即關聯對象的緩衝策略。
如果我們為某個集合類設定了緩衝,如:
<set name="addresses" table="t_address" lazy="true" inverse="true" cascade="all" sort="unsorted"> <cache usage="read-only"/> <key column="user_id"/> <one-to-many class="…TAddress"/></set>
注意這裡的<cache usage=”read-only”/>只會使得Hibernate對資料索引進行緩衝,也就是說,這裡的配置實際上只是緩衝了集合中的資料索引,並不包括這個集合中的各個實體元素。
執行下面的代碼:
TUser user = (TUser)session.load(TUser.class, new Integer(1));Collection addSet = user.getAddresses();//第一次載入user.addressesIterator it = addSet.iterator();while(it.hasNext()){ TAddress addr = (TAddress)it.next(); System.out.println(addr.getAddresses());}System.out.println("\n=== Second Query ===\n");TUser user2 = (TUser)session2.load(TUser.class, new Integer(1));Collection addSet2 = user2.getAddress();//第二次載入user.addressesIterator it2 = addSet2.iterator();while(it2.hasNext()){ TAddress addr = (TAddress)it2.next(); System.out.println(addr.getAddress());}
觀察螢幕日誌輸出:
Hibernate: select tuser0_.id as id3_0_, tuser0_.name as name3_0_, tuser0_.age as age3_0_, tuser0_.group_id as group4_3_0_ from t_user3 tuser0_ where tuser0_.id=?
Hibernate: select addresses0_.user_id as user7_1_, addresses0_.id as id1_, addresses0_.id as id7_0_, addresses0_.address as address7_0_, addresses0_.zipcode as zipcode7_0_, addresses0_.tel as tel7_0_, addresses0_.type as type7_0_, addresses0_.idx as idx7_0_, addresses0_.user_id as user7_7_0_ from t_address addresses0_ where addresses0_.user_id=? order by addresses0_.zipcode asc
Hongkong
Hongkong
=== Second Query ===
Hibernate: select tuser0_.id as id3_0_, tuser0_.name as name3_0_, tuser0_.age as age3_0_, tuser0_.group_id as group4_3_0_ from t_user3 tuser0_ where tuser0_.id=?
Hibernate: select taddress0_.id as id7_0_, taddress0_.address as address7_0_, taddress0_.zipcode as zipcode7_0_, taddress0_.tel as tel7_0_, taddress0_.type as type7_0_, taddress0_.idx as idx7_0_, taddress0_.user_id as user7_7_0_ from t_address taddress0_ where taddress0_.id=?
Hibernate: select taddress0_.id as id7_0_, taddress0_.address as address7_0_, taddress0_.zipcode as zipcode7_0_, taddress0_.tel as tel7_0_, taddress0_.type as type7_0_, taddress0_.idx as idx7_0_, taddress0_.user_id as user7_7_0_ from t_address taddress0_ where taddress0_.id=?
Hongkong
Hongkong
看到,第二次擷取關聯的addresses集合的時候,執行了2次Select SQL。
正是由於<set…><cache usage=”read-only”/>…</set>的設定,第一次addresses集合被載入之後,資料索引已經被放入緩衝。
第二次再載入addresses集合的時候,Hibernate在緩衝中發現了這個資料索引,於是從索引裡面取出當前所有的id此時資料庫中有3條符合的記錄,所以共獲得3個id),然後依次根據3個id在緩衝中尋找對應的實體物件,但是沒有找到,於是發起了資料庫查詢,由Select SQL根據id從t_address表中讀取記錄。
由於緩衝中資料索引的存在,似乎SQL執行的次數更多了,這導致第二次藉助的資料查詢比第一次效能開銷更大。
導致這個問題出現的原因何在?
這是由於我們只為集合類型配置了緩衝,這樣Hibernate只會快取資料索引,而不會將集合中的實體元素同時也納入緩衝。
我們必須為集合類型中的實體物件也指定緩衝策略,如:
<hibernate-mapping> <class name="…TAddress" table="t_address" dynamic-update="false" dynamic-insert="false" select-before-update="false" optimistic-lock="version" > <cache usage="read-write"/> …</hibernate-mapping>
此時,Hibernate才會對集合中的實體也進行緩衝。
再次運行以上代碼:
兩次輸出好像一樣,哪裡有問題?)
上面討論了net.sf.hibernate.collection.Set.iterate方法,同樣,觀察net.sf.hibernate.collection.Set.size/isEmpty方法或者其他hibernate.collection中的同類型方法實現,我們可以看到同樣的處理方式。
通過自訂Collection類型實現資料消極式載入的原理也就在於此。
這樣,通過自身的Collection實現,Hibernate就可以在Collection層從容的實現消極式載入特性。只有程式真正讀取這個Collection的內容時,才激發底層資料庫操作,這為系統的效能提供了更加靈活的調整手段。
3. 屬性的消極式載入
假設t_user表中存在一個長文本類型的Resume欄位,此欄位中儲存了使用者的簡曆資料。長文字欄位的讀取相對而言會帶來較大的效能開銷,因此,我們決定為其設為消極式載入,只有真正需要處理簡曆資訊的時候,才從庫表中讀取。
首先,修改映射設定檔,將Resume欄位的lazy屬性設定為true:
<hibernate-mapping> <class name="…TUser" table="t_user" batch-size="5" > … <property name="resume" type="java.lang.String" column="resume" lazy="true"/> </class></hibernate-mapping>
與實體和集合類型的消極式載入不同,Hibernate3屬性消極式載入機制在配置之外,還需要藉助類增強器對二進位Class檔案進行強化處理buildtime bytecode instrumentation)。
在這裡,我們通過Ant調用Hibernate類增強器對TUser.class檔案進行強化處理。Ant指令碼如下:
<project name="HibernateSample" default="instrument” basedir="."> <property name="lib.dir" value="./lib" /> <property name="classes.dir" value="./bin" /> <path id="lib.class.path"> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> </path> <target name="instrument"> <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask"> <classpath path="${classes.dir}" /> <classpath refid="lib.class.path" /> </taskdef> <instrument verbose="true"> <fileset dir="${classes.dir}/com/redsaga/hibernate/db/entity" > <include name="TUser.class" /> </fileset> </instrument> </target></project>
使用這個指令碼時需要注意各個路徑的配置。本例中,此指令碼位於Eclipse項目的根目錄下,./bin為Eclipse的預設編譯輸出路徑,./bin下存放了執行所需的jar檔案hibernate3.jar及Hibernate所需的類庫)。
以上Ant指令碼將對TUser.class檔案進行強化,如果對其進行反編譯,可以看到如下內容:
package com.redsaga.hibernate.db.entity;import java.io.Serializable;import java.util.Set;import net.sf.cglib.transform.impl.InterceptFieldCallback;import net.sf.cglib.transform.impl.InterceptFieldEnabled;public class TUserimplements Serializable, InterceptFieldEnabled { public InterceptFieldCallback getInterceptFieldCallback(){ return $CGLIB_READ_WRITE_CALLBACK; } public InterceptFieldCallback setInterceptFieldCallback( InterceptFieldCallback interceptFieldcallback){ $CGLIB_READ_WRITE_CALLBACK= interceptFieldcallback; } …略… public String $cglib_read_resume(){ resume; if($CGLIB_READ_WRITE_CALLBACK!=null) goto _L2;else goto _L1; _L1:return; _L2:String s; s; return (String) $CGLIB_READ_WRITE_CALLBACK.readObject(this,”resume”,s); } public void $cglib_write_resume(String s){ resume=$CGLIB_READ_WRITE_CALLBACK == null? s:(String)$CGLIB_READ_WRITE_CALLBACK.writeObject(this, “resume”,resume,s); } …略…}
可以看到,TUser類的內容已經發生了很大的變化。其間,cglib相關代碼被大量植入,通過這些代碼,Hibernate運行期間即可截獲TUser類的方法調用,從而為消極式載入機制提供實現的技術基礎。
經過以上處理,運行以下測試代碼:
String hql = “from TUser user where user.name=’Erica’”;Query query = session.createQuery(hql);List list = query.list();Iterator it = list.iterator();while(it.hasNext()){ TUser user = (TUser)it.next(); System.out.println(user.getName()); System.out.println(user.getResume());}
觀察輸出日誌:
可以看到,在此過程中,Hibernate先後執行了兩條SQL,第一句用於讀取TUser類中的非消極式載入欄位。而之後,當user.getResume()方法調用時,隨即調用第二條SQL從庫表中讀取Resume欄位資料。屬性的消極式載入已經實現。