Hibernate的一對多,多對多映射關係。其中,Collection扮演着數據容器的重要角色。
Hibernate中涉及的Collection類型共有以下幾種:
無序集:Set, Bag, Map
有序集:List
由於傳統的Java Set、Map、List實現不能滿足要求,Hibernate根據這些接口提供了自己的實現。這裏所說的Set、Map和List,均指Hibernate中的實現版本,具體可參見Hibernate源代碼中的net.sf.hibernate.collection包(Hibernate3中對應爲org.hibernate.collection)。
這裏所謂的無序和有序,是針對Hibernate數據持久過程中,是否保持數據集合中的記錄排列順序加以區分的。
對於被定義爲有序集的數據集合,Hibernate在持久化過程中,會將集合中元素排列的先後順序同時固化到數據庫中(以某個特定的字段存儲順序號),下次讀取的時候,也會返回一個具備同樣排列順序的數據集合。
1. Set
Hibernate Set類型的實現位於net.sf.hibernate.collection.Set。通過對java.util.HashSet的封裝,它實現了java.util.Set接口並進行了擴充。
Hibernate Set遵循Java Collection中關於Set的定義,即集合中不允許出現兩個相同元素。如:
TUser user = new TUser(); user.setName(“Emma”); set.add(user); set.add(user); System.out.println(“Item Count in set =>”+set.size());
觀察輸出結果可以看到,雖然執行了兩次add操作,set結構中仍然只包含了一個引用。
對於實體對象與表的映射關係而言,這樣的機制一般不會引發什麼問題,Hibernate在返回關聯對象集合的時候,會自動爲每個記錄構建一個實體對象。如“Hibernate基礎”部分的一對多映射模型中,對應某個t_user表記錄,有3個t_address記錄,那麼Hibernate就會構造3個TAddress對象,放在TUser的addresses集合中返回。即使這3個address記錄的邏輯內容完全相同(id不同)。這個實例對應的映射配置如下:
… <set name="addresses" table="t_address" lazy="false" > <key column="user_id"/> <one-to-many class="org.hibernate.sample.TAddress" /> </set> …
但如果我們並不是通過實體對象(TAddress)進行映射,而是直接通過String對t_address表的address字段進行組合映射,就會觸發一個陷阱。
如在TUser.hbm.xml中進行如下修改:
… <set name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <element type="string" column="address"/> </set> …
上面的配置,直接將t_address表的address字段與TUser相關聯。TUser對象的addresses集合由來自t_address表對應記錄的address字段(String類型)填充。
回憶Java語言基礎中關於字符串比較的內容。
String str1 = “Hello”;
String str2 = “Hello”;
System.out.println(str1 == str2);
運行上面的代碼,我們將得到一個"true"的打印輸出,也就是說,str1和str2在代碼中雖然看上去是兩個字符串對象,其實卻是同一個字符串的引用。
那麼,我們上面的配置會產生怎樣的結果?
顯然,如果t_address表中對應記錄的address字段內容相同,那麼返回的Addresses Set集合中,只會保留一個元素。
TUser user = (TUser)session.load(TUser.class, new Integer(1)); System.out.println("Address Count=>"+user.getAddresses().size());
可以看到,輸出結果爲1。(假設id爲1的user關聯的多個TAddress對象的address字段相同)
查詢如此,而數據刪除也是同樣的道理,Hibernate會刪除所有與指定記錄內容相同的數據,如:
TUser user = (TUser)session.load(TUser.class, new Integer(1)); Transaction tx = session.beginTransaction(); Object obj = user.getAddresses().iterator().next(); user.getAddresses().remove(obj); tx.commit(); session.flush();
只要user_id和address字段內容與條件符合,所有記錄都會被抹去(而往往我們只想刪除某條特定的記錄)。對於這裏的地址數據而言,也許這樣的現象不會造成太大的後果,但是對於一些關鍵數據,則可能造成難以預料的邏輯錯誤。
在使用Set類型的集合類型前,要特別注意這個問題。
爲了補充Set數據類型在這方面的限制,Hibernate提供了Bag類型以供選用。
2. Bag
“Bag”類型在這裏則比較特殊,它是Hibernate自定義的集合類型(Java集合框架中並沒有關於Bag的定義),實現了一個允許包含重複元素的“Set”。
Bag的底層是藉助一個List實現,但卻屏蔽了List的有序特性,也就是說,通過“Bag”聲名的數據集合,其元素排列順序將不會被持久化。
爲了說明Bag的特性,對上面的例子進行一些修改:
<bag name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <element type="string" column="address"/> </bag>
再次運行如下代碼(注意TUser.addresses屬性需對應修改爲List或Collection類型)。
TUser user = (TUser)session.load(TUser.class, new Integer(1)); System.out.println("Address Count=>"+user.getAddresses().size());
可以看到,結果爲3。TUser.addresses中包含了所有的數據記錄。
Bag集合爲無序集,且允許出現重複元素,這也帶來了一個問題。當刪除某個元素的時候,我們該如何定位這條待刪除記錄?由於存在多個相同的元素,我們無法區分各個元素與數據庫記錄的對應關係。
Bag的實現方式,實際上是先將庫表中原有的集合數據全部刪除,再將現有數據逐條插入。無疑這種方式的數據更新的性能是極其低下的。
對於這種情況,如果集合中每個元素都擁有一個id可以唯一檢索到對應的數據庫記錄,那麼問題就迎刃而解。
而idbag,作爲Bag的一種延伸,則成功地解決了這個問題。
idbag配置比Bag多出了一項“collection-id”,用於配置id字段。根據此id字段,Hibernate就可以準確定位庫表記錄,從而實現高效的數據操作。
<idbag name="addresses" lazy="true" table="t_address"> <collection-id type="int" column="id"> <generator class="identity"/> </collection-id> <key column="user_id"/> <element type="string" column="address"/> </idbag>
類似類/表映射關係,這裏我們也可以指定id生成機制。不過,在目前版本中,尚不支持native類型的id生成機制。
3. Map
Map同樣是一個無序集合類型,與Set/Bag不同的是,Map提供了鍵值對應關係。
一個典型的Map配置如下:
<map name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <index type="string" column="type"/> <element type="string" column="address"/> </map>
與Set配置相比,Map增加了一個index配置,用於指定用作Key的字段名:此字段要求在數據集中取值惟一。
之後我們即可在代碼中通過鍵值對集合中的元素進行索引:
TUser user = (TUser)session.load(TUser.class, new Integer(1)); //讀取家庭地址 System.out.println("Home Address is:"+user.getAddresses().get(“Home”)); //讀取辦公地址 System.out.println("Office Address is:"+user.getAddresses().get(“Office”));
type爲TAddress的字段
4. List
與前面幾種不同,List實現了集合內元素順序的持久化。與Map集合需要額外字段保存鍵值一樣,它要求庫表必須配屬對應的字段以保持次序信息。
<list name="addresses" lazy="true" table="t_address"> <key column="user_id"/> <index type="integer" column="idx"/> <element type="string" column="address"/> </list>
index節點中,我們指定以“idx”字段保存次序狀態。
下面的代碼通過交換集合中兩個元素的次序,演示了List集合元素次序的持久化。
TUser user = (TUser)session.load(TUser.class, new Integer(1)); Transaction tx = session.beginTransaction(); //第0和第2項交換位置後保存 Object addr0 = user.getAddresses().get(0); Object addr2 = user.getAddresses().get(2); user.getAddresses().set(0,addr2); user.getAddresses().set(2,addr0); tx.commit(); session.flush();
查看數據庫可以看到兩個位置發生了改變(和idx一致)。
結果集排序:
無序集和有序集,是針對Hibernate數據持久過程中,是否保持數據集合中的記錄排列順序加以區分的。
也就是說,對於一個有序集,其中元素的排列次序將會在庫表中制定的字段中保存,而下次讀取時,也會以同樣的次序排列。
下面所要探討的,則是關於Collection中的元素排序問題。
排序強調的是針對現有數據,以特定的邏輯對其排列次序進行調整。而排序的結果,是數據在內存中的某種排列次序,屬於臨時狀態。
數據排序由兩種方式:
1. Sort
Collection中的數據排序。如對一個List中的元素先後順序進行調整。
2. order-by
對數據庫執行Select SQL時,由order by子句實現的數據排序方式。
可以看出,這兩種排序方式的最基本差異在於,Sort操作是在JVM中完成,而order-by是由數據庫完成。
1. Sort
首先來看一個示例:
<set name=”addresses” lazy=”true” table=”t_address” sort=”naural”> <key column=”user_id”/> <element type=”string” column=”address”/> </set>
可排序Set在Hibernate中對應的實現類爲net.sf.hibernate.collection.SortedSet,它實現了java.util.SortedSet接口。
sort=”natural”指定採用Java默認排序機制,它會調用相應數據類型的compareTo方法進行排序中的值比對。這裏<element type=”string”…>指定了元素類型爲string,也就是說,排序將基於String.compareTo方法。
如果期望指定某種特殊的排序算法,那麼我們可以實現java.util.Comparator接口,並以此實現作爲排序的根據。如下面這段代碼:
/** *基於字符串長度的比對 */ public class LengthComparator implements Comparator{ public int compare(Object obj1, Object obj2){ String str1 = String.valueOf(obj1); String str2 = String.valueOf(obj2); return str1.length()-str2.length(); } }
作爲示例,LengthComparator實現了字符串長度的比對,我們可以在配置中指定LengthComparator作爲排序算法:
<set name=”addresses” lazy=”true” table=”t_address” sort=”org.sample.LengthComparator”> <key column=”user_id”/> <element type=”string” column=”address”/> </set>
Map類型的排序與Set基本一致:
<map name="addresses" lazy="true" table="t_address" sort="org.sample.LengthComparator"> <key column="user_id"/> <element type="string" column="address"/> </map>
index呢???
可排序Map在Hibernate中對應的實現類爲net.sf.hibernate.collection.SortedMap,它實現了java.util.SortedMap接口。
net.sf.hibernate.collection.SortedMap和.SortedSet的內部實現分別基於java.util.TreeSet和java.util.TreeMap。
而Bag和List由於實現原理的不同(且JDK中也並不存在所謂的TreeList),並不支持sort排序方式。
2. order-by
Collection的order-by排序方式,其實現原理也是藉助SQL的order by子句。
同樣來看一個示例:
<set name="addresses" lazy="true" table="t_address" order-by="address desc"> <key column="user_id"/> <element type="string" column="address"/> </set>
在order-by屬性中,我們指定了SQL排序子句。Hibernate在自動生成SQL時,會根據此項配置,自動在SQL中追加相應的order by子句。
運行以下代碼:
TUser user = (TUser)session.load(TUser.class, new Integer(1)); Iterator it = user.getAddresses().iterator(); while(it.hasNext()){ String addr = (String)it.next(); System.out.println(addr); }
觀察Hibernate生成的SQL語句:
可以看到,Hibernate在生成SQL的時候,已經追加了order-by子句“order by addresses0_.address asc”.
注意:
order-by特性在實現中藉助了JDK1.4中的新增集合類LinkedHashSet以及LinkedHashMap,因此order-by特性只支持在1.4版本以上的JDK中運行。
Set、Map、Bag均支持order-by排序,有序集List例外。