Hibernate深入淺出(八)持久層操作——延遲加載(Lazy Loading)

爲了避免在某些情況下,關聯關係所帶來的無謂的性能開銷。

所謂延遲加載,就是在需要數據的時候,才真正執行數據加載操作。

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();//關閉session
Taddress 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.addresses
Iterator 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.addresses
Iterator 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 TUser
implements 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字段數據。屬性的延遲加載已經實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章