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

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