1. 程式人生 > >關於延遲載入(lazy)和強制載入(Hibernate.initialize(Object proxy) )

關於延遲載入(lazy)和強制載入(Hibernate.initialize(Object proxy) )

PO 即Persistence Object 
VO 即Value Object 

PO 和VO 是Hibernate 中兩個比較關鍵的概念。 
首先,何謂VO,很簡單,VO 就是一個簡單的值物件。 
如: 
TUser user = new TUser(); 
user.setName("Emma"); 

這裡的user 就是一個VO。VO 只是簡單攜帶了物件的一些屬性資訊。 
何謂PO? 即納入Hibernate 管理框架中的VO。看下面兩個例子: 

TUser user = new TUser(); 
TUser anotherUser = new TUser(); 
user.setName("Emma"); 

anotherUser.setName("Kevin"); 

//此時user和anotherUser都是VO 

Transaction tx = session.beginTransaction(); 
session.save(user);

//此時的user已經經過Hibernate的處理,成為一個PO ,而anotherUser仍然是個VO 

tx.commit(); 

//事務提交之後,庫表中已經插入一條使用者”Emma”的記錄,對於anotherUser則無任何操作

Transaction tx = session.beginTransaction(); 
user.setName("Emma_1"); //PO 

anotherUser.setName("Kevin_1");//VO 
tx.commit(); 

//事務提交之後,PO的狀態被固化到資料庫中,也就是說資料庫中“Emma”的使用者記錄已經被更新為“Emma_1”,此時anotherUser仍然是個普通Java物件,它的屬性更改不會對資料庫產生任何影響,另外,通過Hibernate返回的物件也是PO: 由Hibernate返回的PO ,如:

TUser user = (TUser)session.load(TUser.class,new Integer(1)); 

VO經過Hibernate進行處理,就變成了PO。上面的示例程式碼session.save(user)中,我們把一個VO “user”傳遞給Hibernate的Session.save方法進行儲存。在save方法中,Hibernate對其進 

行如下處理: 
1. 在當前session所對應的實體容器(Entity Map)中查詢是否存在user物件的引用。 
2. 如果引用存在,則直接返回user物件id,save過程結束. Hibernate中,針對每個Session有一個實體容器(實際上是一個Map物件), 如果此容器中已經儲存了目標物件的引用,那麼hibernate 會認為此物件已經與Session相關聯。 
對於save操作而言,如果物件已經與Session相關聯(即已經被加入Session 的實體容器中),則無需進行具體的操作。因為之後的Session.flush 過程中,Hibernate會對此實體容器中的物件進行遍歷,查找出發生變化的實體,生成並執行相應的update語句。 
3. 如果引用不存在,則根據對映關係,執行insert操作。 
a) 在我們這裡的示例中,採用了native的id生成機制,因此hibernate會從資料庫取得insert操作生成的id並賦予user物件的id屬性。 
b) 將user物件的引用納入Hibernate的實體容器。 
c) save 過程結束,返回物件id. 
而Session.load方法中,再返回物件之前,Hibernate 就已經將此物件納入其實體容器中。

VO和PO的主要區別在於: 

. VO是獨立的Java Object。 
. PO是由Hibernate納入其實體容器(EntityMap)的物件,它代表了與數 
據庫中某條記錄對應的Hibernate實體,PO的變化在事務提交時將反應到實 
際資料庫中。如果一個PO與Session對應的實體容器中分離(如Session 關閉後的PO),那麼 
此時,它又會變成一個VO。

關於unsaved-value 

在非顯示資料儲存時,Hibernate 將根據這個值來判斷物件是否需要儲存。 
所謂顯式儲存,是指程式碼中明確呼叫session 的save 、update 、saveOrupdate 方法對物件進行持久化。如:

session.save(user); 

而在某些情況下,如對映關係中,Hibernate 根據級聯(Cascade )關係對聯接類進行儲存。此時程式碼中沒有針對級聯物件的顯示儲存語句,需要Hibernate 根據物件當前狀 
態判斷是否需要儲存到資料庫。此時,Hibernate 即將根據unsaved-value 進行判定。 
首先Hibernate 會取出目標物件的id。之後,將此值與unsaved-value 進行比對,如果相等,則認為目標物件尚未儲存,否則,認為物件已經儲存,無需再進行儲存操作。 
如:user 物件是之前由hibernate 從資料庫中獲取,同時,此user 物件的若干個關聯物件address 也被載入,此時我們向user 物件新增一個address 物件,此時呼叫 
session.save(user),hibernate 會根據unsaved-value 判斷user 物件的數個address 
關聯物件中,哪些需要執行save 操作,而哪些不需要。

對於我們新加入的address 物件而言,由於其id(Integer 型)尚未賦值,因此為null,與我們設定的unsaved-value(null )相同,因此hibernate 將其視為一個未儲存物件,將為其生成insert 語句並執行。

這裡可能會產生一個疑問,如果“原有”關聯物件發生變動(如user 的某個“原有” 的address 物件的屬性發生了變化,所謂“原有”即此address 物件已經與user 相關聯,而不是我們在此過程中為之新增的),此時id 值是從資料庫中讀出,並沒有發生改變,自然 
與unsaved-value(null)也不一樣,那麼Hibernate 是不是就不儲存了? 

上面關於PO、VO 的討論中曾經涉及到資料儲存的問題,實際上,這裡的“儲存”, 實際上是“insert”的概念,只是針對新關聯物件的加入,而非資料庫中原有關聯物件的 
“update”。所謂新關聯物件,一般情況下可以理解為未與Session 發生關聯的VO。而“原有”關聯物件,則是PO。: 

對於save操作而言,如果物件已經與Session相關聯(即已經被加入Session的實體容器中),則無需進行具體的操作。因為之後的Session.flush 過程中,Hibernate 
會對此實體容器中的物件進行遍歷,查找出發生變化的實體,生成並執行相應的update 語句。

Inverse和Cascade 

Inverse,直譯為“反轉”。在Hibernate語義中,Inverse 指定了關聯關係中的方向。關聯關係中,inverse=”false”的為主動方,由主動方負責維護關聯關係。具體可參見一對多關係中的描述。

而Cascade,譯為“級聯”,表明物件的級聯關係,如TUser 的Cascade設為all, 就表明如果發生對user物件的操作,需要對user所關聯的物件也進行同樣的操作。如對user物件執行save操作,則必須對user物件相關聯的address也執行save操作。

初學者常常混淆inverse和cascade,實際上,這是兩個互不相關的概念。Inverse 指的是關聯關係的控制方向,而cascade指的是層級之間的連鎖操作。

延遲載入(Lazy Loading) 

為了避免一些情況下,關聯關係所帶來的無謂的效能開銷。Hibernate引入了延遲載入的概念。如,示例中user物件在載入的時候,會同時讀取其所關聯的多個地址(address)物件, 
對於需要對address進行操作的應用邏輯而言,關聯資料的自動載入機制的確非常有效。但是,如果我們只是想要獲得user的性別(sex)屬性,而不關心user的地址(address) 
資訊,那麼自動載入address的特性就顯得多餘,並且造成了極大的效能浪費。為了獲得user 的性別屬性,我們可能還要同時從資料庫中讀取數條無用的地址資料,這導致了大量無謂的系統開銷。

延遲載入特性的出現,正是為了解決這個問題。所謂延遲載入,就是在需要資料的時候,才真正執行資料載入操作。

對於我們這裡的user物件的載入過程,也就意味著,載入user物件時只針對其本身的屬性, 而當我們需要獲取user物件所關聯的address資訊時(如執行user.getAddresses時),才 
真正從資料庫中載入address資料並返回。 
嘗試執行以下程式碼:

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已關閉的錯誤。

這裡有個問題,如果我們採用了延遲載入機制,但希望在一些情況下,實現非延遲載入時的功能,也就是說,我們希望在Session關閉後,依然允許操作user的addresses 
屬性。如,為了向View層提供資料,我們必須提供一個完整的User物件,包含其所關聯的address資訊,而這個User物件必須在Session關閉之後仍然可以使用。

Hibernate.initialize方法可以通過強制載入關聯物件實現這一功能: 

Hibernate.initialize(user.getAddresses()); 
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時,必須用JDKCollection 介面(如Set,Map), 而非特定的JDKCollection 實現類(如HashSet、HashMap)申明Collection屬性的原因。