出處:http://www.ibm.com/developerworks/cn/java/j-lo-hibernatelazy/#icommentshtml
Hibernate 的延遲加載(lazy load)是一個被普遍使用的技術。這種延遲加載保證了應用只有在須要時纔去數據庫中抓取相應的記錄。經過延遲加載技術能夠避免過多、過早地加載數據表裏 的數據,從而下降應用的內存開銷。Hibernate 的延遲加載本質上就是代理模式的應用,當程序經過 Hibernate 裝載一個實體時,默認狀況下,Hibernate 並不會當即抓取它的集合屬性、關聯實體因此對應的記錄,而是經過生成一個代理來表示這些集合屬性、關聯實體,這就是代理模式應用帶來的優點。java
Hibernate 的延遲加載(lazy load)是一個被普遍使用的技術。這種延遲加載保證了應用只有在須要時纔去數據庫中抓取相應的記錄。經過延遲加載技術能夠避免過多、過早地加載數據表裏 的數據,從而下降應用的內存開銷。Hibernate 的延遲加載本質上就是代理模式的應用,當程序經過 Hibernate 裝載一個實體時,默認狀況下,Hibernate 並不會當即抓取它的集合屬性、關聯實體因此對應的記錄,而是經過生成一個代理來表示這些集合屬性、關聯實體,這就是代理模式應用帶來的優點。Hibernae 的延遲加載是一個很是經常使用的技術,實體的集合屬性默認會被延遲加載,實體所關聯的實體默認也會被延遲加載。Hibernate 經過這種延遲加載來下降系統的內存開銷,從而保證 Hibernate 的運行性能。程序員
下面先來剖析 Hibernate 延遲加載的「祕密」。web
當 Hibernate 從數據庫中初始化某個持久化實體時,該實體的集合屬性是否隨持久化類一塊兒初始化呢?若是集合屬性裏包含十萬,甚至百萬的記錄,在初始化持久化實體的同時, 完成全部集合屬性的抓取,將致使性能急劇降低。徹底有可能系統只須要使用持久化類集合屬性中的部分記錄,而徹底不是集合屬性的所有,這樣,沒有必要一次加 載全部的集合屬性。數據庫
對於集合屬性,一般推薦使用延遲加載策略。所謂延遲加載就是等系統須要使用集合屬性時才從數據庫裝載關聯的數據。編程
例以下面 Person 類持有一個集合屬性,該集合屬性裏的元素的類型爲 Address,該 Person 類的代碼片斷以下:設計模式
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 能管理該持久化類的集合屬性,程序爲該持久化類提供以下映射文件:網絡
<?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 class="identity"/> </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 class="Address"> <!-- 映射普通屬性 detail --> <property name="detail"/> <!-- 映射普通屬性 zip --> <property name="zip"/> </composite-element> </set> </class> </hibernate-mapping>
從上面映射文件的代碼能夠看出,Person 的集合屬性中的 Address 類只是一個普通的 POJO。該 Address 類裏包含 detail、zip 兩個屬性。因爲 Address 類代碼很是簡單,故此處再也不給出該類的代碼。session
上面映射文件中 <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 對象。此時有兩種狀況:
很明顯,第二種作法既能減小與數據庫的交互,並且避免了裝載 Address 實體帶來的內存開銷——這也是 Hibernate 默認啓用延遲加載的緣由。
如今的問題是,延遲加載究竟是如何實現的呢? Hibernate 在加載 Person 實體時,Person 實體的 addresses 屬性值是什麼呢?
爲了解決這個問題,咱們在 <1>
號代碼處設置一個斷點,在 Eclipse 中進行 Debug,此時能夠看到 Eclipse 的 Console 窗口有如圖 1 所示的輸出:
正如圖 1 輸出所看到的,此時 Hibernate 只從 Person 實體對應的數據表中抓取數據,並未從 Address 對象對應的數據表中抓取數據,這就是延遲加載。
那麼 Person 實體的 addresses 屬性是什麼呢?此時能夠從 Eclipse 的 Variables 窗口看到如圖 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 能夠看出,此時的 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 默認都會採用延遲加載。
對於關聯實體,能夠將其分爲兩種狀況:
當關聯實體是單個實體時,也就是使用 <many-to-one.../> 或 <one-to-one.../> 映射關聯實體的情形,這兩個元素也可經過 lazy 屬性來指定延遲加載。
下面例子把 Address 類也映射成持久化類,此時 Address 類也變成實體類,Person 實體與 Address 實體造成一對多的雙向關聯。此時的映射文件代碼以下:
<?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 class="identity"/> </id> <!-- 用於映射普通屬性 --> <property name="name" type="string"/> <property name="age" type="int"/> <!-- 映射集合屬性,集合元素是其餘持久化實體 沒有指定 cascade 屬性,指定不控制關聯關係 --> <set name="addresses" inverse="true"> <!-- 指定關聯的外鍵列 --> <key column="person_id"/> <!-- 用以映射到關聯類屬性 --> <one-to-many class="Address"/> </set> </class> <!-- 映射 Address 持久化類 --> <class name="Address" table="address_inf"> <!-- 映射標識屬性 addressId --> <id name="addressId" column="address_id"> <!-- 指定主鍵生成器策略 --> <generator class="identity"/> </id> <!-- 映射普通屬性 detail --> <property name="detail"/> <!-- 映射普通屬性 zip --> <property name="zip"/> <!-- 必須指定列名爲 person_id, 與關聯實體中 key 元素的 column 屬性值相同 --> <many-to-one name="person" class="Person" column="person_id" not-null="true"/> </class> </hibernate-mapping>
接下來程序經過以下代碼片斷來加載 ID 爲 1 的 Person 實體:
// 打開上下文相關的 Session Session 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 能夠清楚地看到,此時 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 所示的結果:
Hibernate 採用「延遲加載」管理關聯實體的模式,其實就在加載主實體時,並未真正去抓取關聯實體對應數據,而只是動態地生成一個對象做爲關聯實體的代理。當應用程序真正須要使用關聯實體時,代理對象會負責從底層數據庫抓取記錄,並初始化真正的關聯實體。
在 Hibernate 的延遲加載中,客戶端程序開始獲取的只是一個動態生成的代理對象,而真正的實體則委託給代理對象來管理——這就是典型的代理模式。
代理模式是一種應用很是普遍的設計模式,當客戶端代碼須要調用某個對象時,客戶端實際上也不關心是否準確獲得該對象,它只要一個能提供該功能的對象便可,此時咱們就可返回該對象的代理(Proxy)。
在這種設計方式下,系統會爲某個對象提供一個代理對象,並由代理對象控制對源對象的引用。代理就是一個 Java 對象表明另外一個 Java 對象來採起行動。在某些狀況下,客戶端代碼不想或不可以直接調用被調用者,代理對象能夠在客戶和目標對象之間起到中介的做用。
對客戶端而言,它不能分辨出代理對象與真實對象的區別,它也無須分辨代理對象和真實對象的區別。客戶端代碼並不知道真正的被代理對象,客戶端代碼面向接口編程,它僅僅持有一個被代理對象的接口。
總而言之,只要客戶端代碼不能或不想直接訪問被調用對象——這種狀況有不少緣由,好比須要建立一個系統開銷很大的對象,或者被調用對象在遠程主機上,或者目標對象的功能還不足以知足需求……,而是額外建立一個代理對象返回給客戶端使用,那麼這種設計方式就是代理模式。
下面示範一個簡單的代理模式,程序首先提供了一個 Image 接口,表明大圖片對象所實現的接口,該接口代碼以下:
public interface Image { void show(); }
該接口提供了一個實現類,該實現類模擬了一個大圖片對象,該實現類的構造器使用 Thread.sleep() 方法來暫停 3s。下面是該 BigImage 的程序代碼。
// 使用該 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 類的代理類以下所示。
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 的代理對象,以下面程序所示。
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 所示的運行結果,讀者應該能認同:使用代理模式提升了獲取 Image 對象的系統性能。但可能有讀者會提出疑問:程序調用 ImageProxy 對象的 show() 方法時同樣須要建立 BigImage 對象啊,系統開銷並未真正減小啊?只是這種系統開銷延遲了而已啊?
咱們能夠從以下兩個角度來回答這個問題:
與此徹底相似的是,Hibernate 也是經過代理模式來「推遲」加載關聯實體的時間,若是程序並不須要訪問關聯實體,那程序就不會去抓取關聯實體了,這樣既能夠節省系統的內存開銷,也能夠縮短 Hibernate 加載實體的時間。
Hibernate 的延遲加載(lazy load)本質上就是代理模式的應用,咱們在過去的歲月裏就常常經過代理模式來下降系統的內存開銷、提高應用的運行性能。Hibernate 充分利用了代理模式的這種優點,並結合了 Javassist 或 CGLIB 來動態地生成代理對象,這更加增長了代理模式的靈活性,Hibernate 給這種用法一個新名稱:延遲加載。不管怎樣,充分分析、瞭解這些開源框架的實現能夠更好的感覺經典設計模式的優點所在。