Hibernate深入淺出(十)Collection&結果集排序

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例外。

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