本文實例講述了Hibernate延遲加載技術。分享給大家供大家參考,具體如下:
Hibernae 的延遲加載是一個非常常用的技術,實體的集合屬性默認會被延遲加載,實體所關聯的實體默認也會被延遲加載。 Hibernate 通過這種延遲加載來降低系統的內存開銷,從而保證Hibernate 的運行性能。
下面先來剖析Hibernate 延遲加載的“秘密”。
集合屬性的延遲加載
當Hibernate 從數據庫中初始化某個持久化實體時,該實體的集合屬性是否隨持久化類一起初始化呢?如果集合屬性裡包含十萬,甚至百萬的記錄,在初始化持久化實體的同時,完成所有集合屬性的抓取,將導致性能急劇下降。完全有可能係統只需要使用持久化類集合屬性中的部分記錄,而完全不是集合屬性的全部,這樣,沒有必要一次加載所有的集合屬性。
對於集合屬性,通常推薦使用延遲加載策略。所謂延遲加載就是等系統需要使用集合屬性時才從數據庫裝載關聯的數據。
例如下面Person 類持有一個集合屬性,該集合屬性裡的元素的類型為Address,該Person 類的代碼片段如下:
清單1. Person.java
public class Person{ // 標識屬性private Integer id; // Person 的name 屬性private String name; // 保留Person 的age 屬性private int age; // 使用Set 來保存集合屬性private Set<Address> addresses = new HashSet<Address>(); // 下面省略了各屬性的setter 和getter 方法...}為了讓Hibernate 能管理該持久化類的集合屬性,程序為該持久化類提供如下映射文件:
清單2. Person.hbm.xml
<?xml version="1.0" encoding="GBK"?><!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"><hibernate-mapping package="org.crazyit.app.domain"><!-- 映射Person 持久化類--><class name="Person" table="person_inf"><!-- 映射標識屬性id --><id name="id" column="person_id"><!-- 定義主鍵生成器策略--><generator/></id><!-- 用於映射普通屬性--><property name="name" type="string"/><property name="age" type="int"/><!-- 映射集合屬性--><set name="addresses" table="person_address" lazy="true"><!-- 指定關聯的外鍵列--><key column="person_id"/><composite-element><!-- 映射普通屬性detail --><property name="detail"/><!-- 映射普通屬性zip --><property name="zip"/></composite-element></set></class></hibernate-mapping>
從上面映射文件的代碼可以看出,Person 的集合屬性中的Address 類只是一個普通的POJO。該Address 類裡包含detail、zip 兩個屬性。由於Address 類代碼非常簡單,故此處不再給出該類的代碼。
上面映射文件中<set.../> 元素裡的代碼指定了lazy="true"(對於<set.../> 元素來說,lazy="true"是默認值),它指定Hibernate 會延遲加載集合屬性裡Address 對象。
例如通過如下代碼來加載ID 為1 的Person 實體:
Session session = sf.getCurrentSession();Transaction tx = session.beginTransaction();Person p = (Person) session.get(Person.class, 1); //<1>System.out.println(p.getName());
上面代碼只是需要訪問ID 為1 的Person 實體,並不想訪問這個Person 實體所關聯的Address 對象。此時有兩種情況:
1. 如果不延遲加載,Hibernate 就會在加載Person 實體對應的數據記錄時立即抓取它關聯的Address 對象。
2. 如果採用延遲加載,Hibernate 就只加載Person 實體對應的數據記錄。
很明顯,第二種做法既能減少與數據庫的交互,而且避免了裝載Address 實體帶來的內存開銷――這也是Hibernate 默認啟用延遲加載的原因。
現在的問題是,延遲加載到底是如何實現的呢? Hibernate 在加載Person 實體時,Person 實體的addresses 屬性值是什麼呢?
為了解決這個問題,我們在<1>號代碼處設置一個斷點,在Eclipse 中進行Debug,此時可以看到Eclipse 的Console 窗口有如圖1 所示的輸出:
圖1. 延遲加載集合屬性的Console 輸出
正如圖1 輸出所看到的,此時Hibernate 只從Person 實體對應的數據表中抓取數據,並未從Address 對像對應的數據表中抓取數據,這就是延遲加載。
那麼Person 實體的addresses 屬性是什麼呢?此時可以從Eclipse 的Variables 窗口看到如圖2 所示的結果:
圖2. 延遲加載的集合屬性值
從圖2 的方框裡的內容可以看出,這個addresses 屬性並不是我們熟悉的HashSet、TreeSet 等實現類,而是一個PersistentSet 實現類,這是Hibernate 為Set 接口提供的一個實現類。
PersistentSet 集合對象並未真正抓取底層數據表的數據,因此自然也無法真正去初始化集合裡的Address 對象。不過PersistentSet 集合裡持有一個session 屬性,這個session 屬性就是Hibernate Session,當程序需要訪問PersistentSet 集合元素時,PersistentSet 就會利用這個session 屬性去抓取實際的Address 對像對應的數據記錄。
那麼到底抓取那些Address 實體對應的數據記錄呢?這也難不倒PersistentSet,因為PersistentSet 集合裡還有一個owner 屬性,該屬性就說明了Address 對象所屬的Person 實體,Hibernate 就會去查找Address 對應數據表中外鍵值參照到該Person 實體的數據。
例如我們單擊圖2 所示窗口中addresses 行,也就是告訴Eclipse 要調試、輸出addresses 屬性,這就是要訪問addresses 屬性了,此時就可以在Eclipse 的Console 窗口看到輸出如下SQL 語句:
select addresses0_.person_id as person1_0_0_, addresses0_.detail as detail0_, addresses0_.zip as zip0_from person_address addresses0_where addresses0_.person_id=?
這就是PersistentSet 集合跟據owner 屬性去抓取特定Address 記錄的SQL 語句。此時可以從Eclipse 的Variables 窗口看到圖3 所示的輸出:
圖3. 已加載的集合屬性值
從圖3 可以看出,此時的addresses 屬性已經被初始化了,集合裡包含了2 個Address 對象,這正是Person 實體所關聯的兩個Address 對象。
通過上面介紹可以看出,Hibernate 對於Set 屬性延遲加載關鍵就在於PersistentSet 實現類。在延遲加載時,開始PersistentSet 集合裡並不持有任何元素。但PersistentSet 會持有一個Hibernate Session,它可以保證當程序需要訪問該集合時“立即”去加載數據記錄,並裝入集合元素。
與PersistentSet 實現類類似的是,Hibernate 還提供了PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 等實現類,它們的功能與PersistentSet 的功能大致類似。
熟悉Hibernate 集合屬性讀者應該記得:Hibernate 要求聲明集合屬性只能用Set、List、Map、SortedSet、SortedMap 等接口,而不能用HashSet、ArrayList、HashMap、TreeSet、TreeMap 等實現類,其原因就是因為Hibernate 需要對集合屬性進行延遲加載,而Hibernate 的延遲加載是依靠PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 來完成的――也就是說,Hibernate 底層需要使用自己的集合實現類來完成延遲加載,因此它要求開發者必須用集合接口、而不是集合實現類來聲明集合屬性。
Hibernate 對集合屬性默認採用延遲加載,在某些特殊的情況下,為<set.../>、<list.../>、<map.../> 等元素設置lazy="false"屬性來取消延遲加載。
關聯實體的延遲加載
默認情況下,Hibernate 也會採用延遲加載來加載關聯實體,不管是一對多關聯、還是一對一關聯、多對多關聯,Hibernate 默認都會採用延遲加載。
對於關聯實體,可以將其分為兩種情況:
1. 關聯實體是多個實體時(包括一對多、多對多):此時關聯實體將以集合的形式存在,Hibernate 將使用PersistentSet、PersistentList、PersistentMap、PersistentSortedMap、PersistentSortedSet 等集合來管理延遲加載的實體。這就是前面所介紹的情形。
2. 關聯實體是單個實體時(包括一對一、多對一):當Hibernate 加載某個實體時,延遲的關聯實體將是一個動態生成代理對象。
當關聯實體是單個實體時,也就是使用<many-to-one.../> 或<one-to-one.../> 映射關聯實體的情形,這兩個元素也可通過lazy 屬性來指定延遲加載。
下面例子把Address 類也映射成持久化類,此時Address 類也變成實體類,Person 實體與Address 實體形成一對多的雙向關聯。此時的映射文件代碼如下:
清單3. Person.hbm.xml
<?xml version="1.0" encoding="GBK"?><!-- 指定Hibernate 的DTD 信息--><!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"><hibernate-mapping package="org.crazyit.app.domain"><!-- 映射Person 持久化類--><class name="Person" table="person_inf"><!-- 映射標識屬性id --><id name="id" column="person_id"><!-- 定義主鍵生成器策略--><generator/></id><!-- 用於映射普通屬性--><property name="name" type="string"/><property name="age" type="int"/><!-- 映射集合屬性,集合元素是其他持久化實體沒有指定cascade 屬性,指定不控制關聯關係--><set name="addresses" inverse="true"><!-- 指定關聯的外鍵列--><key column="person_id"/><!-- 用以映射到關聯類屬性--><one-to-many/></set></class><!-- 映射Address 持久化類--><class name="Address" table="address_inf"><!-- 映射標識屬性addressId --><id name="addressId" column="address_id"><!-- 指定主鍵生成器策略--><generator/></id><!-- 映射普通屬性detail --><property name="detail"/><!-- 映射普通屬性zip --><property name="zip"/><!-- 必須指定列名為person_id,與關聯實體中key 元素的column 屬性值相同--><many-to-one name="person"column="person_id" not-null="true"/></class></hibernate-mapping>
接下來程序通過如下代碼片段來加載ID 為1 的Person 實體:
// 打開上下文相關的SessionSession session = sf.getCurrentSession();Transaction tx = session.beginTransaction();Address address = (Address) session.get(Address.class , 1); //<1>System.out.println(address.getDetail());
為了看到Hibernate 加載Address 實體時對其關聯實體的處理,我們在<1>號代碼處設置一個斷點,在Eclipse 中進行Debug,此時可以看到Eclipse 的Console 窗口輸出如下SQL 語句:
select address0_.address_id as address1_1_0_, address0_.detail as detail1_0_, address0_.zip as zip1_0_, address0_.person_id as person4_1_0_from address_inf address0_where address0_.address_id=?
從這條SQL 語句不難看出,Hibernate 加載Address 實體對應的數據表抓取記錄,並未從Person 實體對應的數據表中抓取記錄,這是延遲加載發揮了作用。
從Eclipse 的Variables 窗口看到如圖4 所示的輸出:
圖4. 延遲加載的實體
從圖4 可以清楚地看到,此時Address 實體所關聯的Person 實體並不是Person 對象,而是一個Person_$$_javassist_0 類的實例,這個類是Hibernate 使用Javassist 項目動態生成的代理類――當Hibernate 延遲加載關聯實體時,將會採用Javassist 生成一個動態代理對象,這個代理對象將負責代理“暫未加載”的關聯實體。
只要應用程序需要使用“暫未加載”的關聯實體,Person_$$_javassist_0 代理對象會負責去加載真正的關聯實體,並返回實際的關聯實體――這就是最典型的代理模式。
單擊圖4 所示Variables 窗口中的person 屬性(也就是在調試模式下強行使用person 屬性),此時看到Eclipse 的Console 窗口輸出如下的SQL 語句:
select person0_.person_id as person1_0_0_, person0_.name as name0_0_, person0_.age as age0_0_from person_inf person0_where person0_.person_id=?
上面SQL 語句就是去抓取“延遲加載”的關聯實體的語句。此時可以看到Variables 窗口輸出圖5 所示的結果:
圖5. 已加載的實體
Hibernate 採用“延遲加載”管理關聯實體的模式,其實就在加載主實體時,並未真正去抓取關聯實體對應數據,而只是動態地生成一個對像作為關聯實體的代理。當應用程序真正需要使用關聯實體時,代理對象會負責從底層數據庫抓取記錄,並初始化真正的關聯實體。
在Hibernate 的延遲加載中,客戶端程序開始獲取的只是一個動態生成的代理對象,而真正的實體則委託給代理對象來管理――這就是典型的代理模式。
代理模式
代理模式是一種應用非常廣泛的設計模式,當客戶端代碼需要調用某個對象時,客戶端實際上也不關心是否準確得到該對象,它只要一個能提供該功能的對象即可,此時我們就可返回該對象的代理(Proxy)。
在這種設計方式下,系統會為某個對象提供一個代理對象,並由代理對象控制對源對象的引用。代理就是一個Java 對象代表另一個Java 對象來採取行動。在某些情況下,客戶端代碼不想或不能夠直接調用被調用者,代理對象可以在客戶和目標對象之間起到中介的作用。
對客戶端而言,它不能分辨出代理對象與真實對象的區別,它也無須分辨代理對象和真實對象的區別。客戶端代碼並不知道真正的被代理對象,客戶端代碼面向接口編程,它僅僅持有一個被代理對象的接口。
總而言之,只要客戶端代碼不能或不想直接訪問被調用對象――這種情況有很多原因,比如需要創建一個系統開銷很大的對象,或者被調用對像在遠程主機上,或者目標對象的功能還不足以滿足需求……,而是額外創建一個代理對象返回給客戶端使用,那麼這種設計方式就是代理模式。
下面示範一個簡單的代理模式,程序首先提供了一個Image 接口,代表大圖片對象所實現的接口,該接口代碼如下:
清單3. Image.java
public interface Image{void show();}該接口提供了一個實現類,該實現類模擬了一個大圖片對象,該實現類的構造器使用Thread.sleep() 方法來暫停3s。下面是該BigImage 的程序代碼。
清單4. BigImage.java
// 使用該BigImage 模擬一個很大圖片public class BigImage implements Image{public BigImage(){try{// 程序暫停3s 模式模擬系統開銷Thread.sleep(3000);System.out.println("圖片裝載成功...");}catch (InterruptedException ex){ex.printStackTrace();}}// 實現Image 裡的show() 方法public void show(){System.out.println("繪製實際的大圖片");}}上面的程序代碼暫停了3s,這表明創建一個BigImage 對象需要3s 的時間開銷――程序使用這種延遲來模擬裝載此圖片所導致的系統開銷。如果不採用代理模式,當程序中創建BigImage 時,系統將會產生3s 的延遲。為了避免這種延遲,程序為BigImage 對象提供一個代理對象,BigImage 類的代理類如下所示。
清單5. ImageProxy.java
public class ImageProxy implements Image{// 組合一個image 實例,作為被代理的對象private Image image;// 使用抽象實體來初始化代理對象public ImageProxy(Image image){this.image = image;}/*** 重寫Image 接口的show() 方法* 該方法用於控制對被代理對象的訪問,* 並根據需要負責創建和刪除被代理對象*/public void show(){// 只有當真正需要調用image 的show 方法時才創建被代理對象if (image == null){ image = new BigImage();}image.show();}}上面的ImageProxy 代理類實現了與BigImage 相同的show() 方法,這使得客戶端代碼獲取到該代理對象之後,可以將該代理對象當成BigImage 來使用。
在ImageProxy 類的show() 方法中增加了控制邏輯,這段控制邏輯用於控制當系統真正調用image 的show() 時,才會真正創建被代理的BigImage 對象。下面程序需要使用BigImage 對象,但程序並不是直接返回BigImage 實例,而是先返回BigImage 的代理對象,如下面程序所示。
清單6. BigImageTest.java
public class BigImageTest{public static void main(String[] args){long start = System.currentTimeMillis();// 程序返回一個Image 對象,該對像只是BigImage 的代理對象Image image = new ImageProxy(null);System.out.println("系統得到Image 對象的時間開銷:" +(System.currentTimeMillis() - start));// 只有當實際調用image 代理的show() 方法時,程序才會真正創建被代理對象。 image.show();}}上面程序初始化image 非常快,因為程序並未真正創建BigImage 對象,只是得到了ImageProxy 代理對象――直到程序調用image.show() 方法時,程序需要真正調用BigImage 對象的show() 方法,程序此時才真正創建BigImage 對象。運行上面程序,看到如圖6 所示的結果。
圖6. 使用代理模式提高性能
看到如圖6 所示的運行結果,讀者應該能認同:使用代理模式提高了獲取Image 對象的系統性能。但可能有讀者會提出疑問:程序調用ImageProxy 對象的show() 方法時一樣需要創建BigImage 對像啊,系統開銷並未真正減少啊?只是這種系統開銷延遲了而已啊?
我們可以從如下兩個角度來回答這個問題:
把創建BigImage 推遲到真正需要它時才創建,這樣能保證前面程序運行的流暢性,而且能減少BigImage 在內存中的存活時間,從宏觀上節省了系統的內存開銷。
有些情況下,也許程序永遠不會真正調用ImageProxy 對象的show() 方法――意味著系統根本無須創建BigImage 對象。在這種情形下,使用代理模式可以顯著地提高系統運行性能。
與此完全類似的是,Hibernate 也是通過代理模式來“推遲”加載關聯實體的時間,如果程序並不需要訪問關聯實體,那程序就不會去抓取關聯實體了,這樣既可以節省系統的內存開銷,也可以縮短Hibernate 加載實體的時間。
小結
Hibernate 的延遲加載(lazy load)本質上就是代理模式的應用,我們在過去的歲月裡就經常通過代理模式來降低系統的內存開銷、提升應用的運行性能。 Hibernate 充分利用了代理模式的這種優勢,並結合了Javassist 或CGLIB 來動態地生成代理對象,這更加增加了代理模式的靈活性,Hibernate 給這種用法一個新名稱:延遲加載。無論怎樣,充分分析、了解這些開源框架的實現可以更好的感受經典設計模式的優勢所在。
希望本文所述對大家基於Hibernate框架的Java程序設計有所幫助。