Chapter 6. 集合类(Collections)

6.1. 持久化集合类(Persistent Collections)

(译者注:在阅读本章的时候,以后整个手册的阅读过程中,我们都会面临一个名词方面的问题,那就是“集合”。"Collections"和"Set"在中文里对应都被翻译为“集合”,但是他们的含义很不一样。Collections是一个超集,Set是其中的一种。大部分情况下,本译稿中泛指的未加英文注明的“集合”,都应当理解为“Collections”。在有些二者同时出现,可能造成混淆的地方,我们用“集合类”来特指“Collecions”,“集合(Set)”来指"Set",一般都会在后面的括号中给出英文。希望大家在阅读时联系上下文理解,不要造成误解。 与此同时,“元素”一词对应的英文“element”,也有两个不同的含义。其一为集合的元素,是内存中的一个变量;另一含义则是XML文档中的一个标签所代表的元素。也请注意区别。 本章中,特别是后半部分是需要反复阅读才能理解清楚的。如果遇到任何疑问,请记住,英文版本的reference是惟一标准的参考资料。)

这部分不包含大量的Java代码例子。我们假定你已经了解如何使用Java自身的集合类框架(Java's collections framework)。 其实如果是这样, 这里就真的没有什么东西需要学习了... 用一句话来做个总结,你就用你已经掌握的知识来使用它们吧,不用为了适应Hibernate而作出改变.

Hibernate可以持久化以下java集合的实例, 包括java.util.Map, java.util.Set, java.util.SortedMap, java.util.SortedSet, java.util.List, 和任何持久实体或值的数组。类型为java.util.Collection或者java.util.List的属性还可以使用"bag"语义来持久。

警告:用于持久化的集合,除了集合接口外,不能保留任何实现这些接口的类所附加的语义(例如:LinkedHashSet带来的迭代顺序)。所有的持久化集合,实际上都各自按照 HashMap, HashSet, TreeMap, TreeSetArrayList 的语义直接工作。更深入地说,对于一个包含集合的属性来说,必须把Java类型定义为接口(也就是Map, Set 或者List等),而绝不能是HashMap, TreeSet 或者 ArrayList。存在这个限制的原因是,在你不知道的时候,Hibernate暗中把你的Map, SetList 的实例替换成了它自己的关于Map, Set 或者 List 的实现。(所以在你的程序中,谨慎使用==操作符。)(译者说明: 为了提高性能等方面的原因,在Hibernate中实现了几乎所有的Java集合的接口 。)

Cat cat = new DomesticCat();
Cat kitten = new DomesticCat();
....
Set kittens = new HashSet();
kittens.add(kitten);
cat.setKittens(kittens);
session.save(cat);
kittens = cat.getKittens(); //Okay, kittens collection is a Set
(HashSet) cat.getKittens(); //Error!

集合遵从对值类型的通常规则:不能共享引用, 与其包含的实体共存亡。由于存在底层的关联模型,集合不支持空值语义;并且hibernate不会区分一个null的集合引用和一个不存在元素的空集合。

集合实例在数据库中根据指向对应实体的外键而得到区别。这个外键被称为集合的关键字。在Hibernate配置文件中使用<key> 元素来映射这个集合的关键字。

集合可以包含几乎所有的Hibernate类型, 包括所有的基本类型, 自定义类型,实体类型和组件。集合不能包含其他集合。这些被包含的元素的类型被称为集合元素类型。集合的元素在Hibernate中被映射为<element>, <composite-element>, <one-to-many>, <many-to-many> 或者 <many-to-any>

除了SetBag之外的所有集合类型都有一个索引(index)字段,这个字段映射到一个数组或者List的索引或者Map的key。Map的索引的类型可以是任何基本类型, 实体类型或者甚至是一个组合类型(但不能是一个集合类型)。数组和list的索引肯定是整型,integer。在Hibernate配置文件中使用 <index>, <index-many-to-many>, <composite-index> 或者 <index-many-to-any>等元素来映射索引。

集合类可以产生相当多种类的映射,涵盖了很多通常的关系模型。我们建议你练习使用schema生成工具, 以便对如何把不同的映射定义转换为数据库表有一个感性认识。

6.2. 映射集合(Mapping a Collection)

在Hibernate配置文件中使用<set>, <list>, <map>, <bag>, <array><primitive-array>等元素来定义集合,而<map>是最典型的一个。

<map
    name="propertyName"                                         (1)
    table="table_name"                                          (2)
    schema="schema_name"                                        (3)
    lazy="true|false"                                           (4)
    inverse="true|false"                                        (5)
    cascade="all|none|save-update|delete|all-delete-orphan"     (6)
    sort="unsorted|natural|comparatorClass"                     (7)
    order-by="column_name asc|desc"                             (8)
    where="arbitrary sql where condition"                       (9)
    outer-join="true|false|auto"                                (10)
    batch-size="N"                                              (11)
    access="field|property|ClassName"                           (12)
>

    <key .... />
    <index .... />
    <element .... />
</map>
1

name 集合属性的名称

2

table (可选——默认为属性的名称)这个集合表的名称(不能在一对多的关联关系中使用)

3

schema (可选) 表的schema的名称, 他将覆盖在根元素中定义的schema

4

lazy (可选——默认为false) lazy(可选--默认为false) 允许延迟加载(lazy initialization )(不能在数组中使用)

5

inverse (可选——默认为false) 标记这个集合作为双向关联关系中的方向一端。

6

cascade (可选——默认为none) 让操作级联到子实体

7

sort(可选)指定集合的排序顺序, 其可以为自然的(natural)或者给定一个用来比较的类。

8

order-by (可选, 仅用于jdk1.4) 指定表的字段(一个或几个)再加上asc或者desc(可选), 定义Map,Set和Bag的迭代顺序

9

where (可选) 指定任意的SQL where条件, 该条件将在重新载入或者删除这个集合时使用(当集合中的数据仅仅是所有可用数据的一个子集时这个条件非常有用)

10

outer-join(可选)指定这个集合,只要可能,应该通过外连接(outer join)取得。在每一个SQL语句中, 只能有一个集合可以被通过外连接抓取(译者注: 这里提到的SQL语句是取得集合所属类的数据的Select语句)

(11)

batch-size (可选, 默认为1) 指定通过延迟加载取得集合实例的批处理块大小("batch size")。

(12)

access(可选-默认为属性property):Hibernate取得属性值时使用的策略

建立列表(List)数组(Array)需要一个单独表字段用来保存列表(List)或数组(Array)的索引(foo[i]中的i)。如果你的关系模型中没有一个索引字段, 例如:如果你处理的是老式的遗留数据, 你可以用无序的Set来替代。这会让那些以为List应该是访问无序集合的比较方便的方法的人感到气馁。Hibernate集合严格遵守Set,List和Map接口中包涵的自然语义。 List元素不能正确的自发对他们自己进行排序!

在另一方面, 那些准备使用List来模拟bag的语义的人有一个合法的委屈(a legitimate grievance)。bag是一个无序,没有索引的集合并且可能包含多个相同的元素。在Java集合框架中没有Bag接口(虽然你可以用List模拟它)。Hibernate允许你映射类型为List或者Collection的属性到<bag>元素。注意: Bag语义事实上并不是Collection规范(contract)的一部分并且事实上它和List规范中的语义是相矛盾的。

具有inverse="false"标记的大型Hibernate bag效率是相当低的,应该尽量避免。Hibernate无法创建,删除和更新它的单个记录, 因为他们没有关键字来识别单个记录。

6.3. 值集合和多对多关联(Collections of Values and Many To Many Associations)

任何值集合和实体集合如果被映射为多对多关联(Java集合中的语义)就需要一个集合表。这个表中包含外键字段,元素字段还可能有索引字段。

使用<key>元素来申明从集合表到其拥有者类的表(from the collection table to the table of the owning class)的外键关键字。

<key column="column_name"/>
1

column(必需):外键字段的名称

对于类似与map和list的带索引的集合, 我们需要一个<index>元素。对于list来说, 这个字段包含从零开始的连续整数。对于map来说,这个字段可以包含任意Hibernate类型的值。

<index
        column="column_name"                (1)
        type="typename"                     (2)
/>
1

column(必需):保存集合索引值的字段名。

2

type (可选,默认为整型integer):集合索引的类型。

还有另外一个选择,map可以是实体类型的对象。在这里我们使用<index-many-to-many>元素。

<index-many-to-many
        column="column_name"                (1)
        class="ClassName"                   (2)
/>
1

column(必需):集合索引值中外键字段的名称

2

class (required):(必需):集合的索引使用的实体类。

对于一个值集合, 我们使用<element>标签。

<element
        column="column_name"                (1)
        type="typename"                     (2)
/>
1

column(必需):保存集合元素值的字段名。

2

type (必需):集合元素的类型

一个拥有自己表的实体集合对应于多对多(many-to-many)关联关系概念。多对多关联是针对Java集合的最自然映射关联关系,但通常并不是最好的关系模型。

<many-to-many
        column="column_name"                               (1)
        class="ClassName"                                  (2)
        outer-join="true|false|auto"                       (3)
/>
1

column(必需): 这个元素的外键关键字段名

2

class (必需): 关联类的名称

3

outer-join (可选 - 默认为auto): 在Hibernate系统参数中hibernate.use_outer_join被打开的情况下,该参数用来允许使用outer join来载入此集合的数据。

例子:

首先, 一组字符串:

<set name="names" table="NAMES">
    <key column="GROUPID"/>
    <element column="NAME" type="string"/>
</set>

包含一组整数的bag(还设置了order-by参数指定了迭代的顺序):

<bag name="sizes" table="SIZES" order-by="SIZE ASC">
    <key column="OWNER"/>
    <element column="SIZE" type="integer"/>
</bag>

一个实体数组,在这个案例中是一个多对多的关联(注意这里的实体是自动管理生命周期的对象(lifecycle objects),cascade="all"):

<array name="foos" table="BAR_FOOS" cascade="all">
    <key column="BAR_ID"/>
    <index column="I"/>
    <many-to-many column="FOO_ID" class="com.illflow.Foo"/>
</array>

一个map,通过字符串的索引来指明日期:

<map name="holidays" table="holidays" schema="dbo" order-by="hol_name asc">
    <key column="id"/>
    <index column="hol_name" type="string"/>
    <element column="hol_date" type="date"/>
</map>

一个组件的列表:

<list name="carComponents" table="car_components">
    <key column="car_id"/>
    <index column="posn"/>
    <composite-element class="com.illflow.CarComponent">
            <property name="price" type="float"/>
            <property name="type" type="com.illflow.ComponentType"/>
            <property name="serialNumber" column="serial_no" type="string"/>
    </composite-element>
</list>

6.4. 一对多关联(One To Many Associations)

一对多关联直接连接两个类对应的表,而没有中间集合表。(这实现了一个一对多的关系模型)(译者注:这有别与多对多的关联需要一张中间表)。 这个关系模型失去了一些Java集合的语义:

  • map,set或list中不能包含null值

  • 一个被包含的实体的实例只能被包含在一个集合的实例中

  • 一个被包含的实体的实例只能对应于集合索引的一个值中

一个从FooBar的关联需要额外的关键字字段,可能还有一个索引字段指向这个被包含的实体类,Bar所对应的表。这些字段在映射时使用前面提到的<key><index>元素。

<one-to-many>标记指明了一个一对多的关联。

<one-to-many class="ClassName"/>
1

class(必须):被关联类的名称。

例子

<set name="bars">
    <key column="foo_id"/>
    <one-to-many class="com.illflow.Bar"/>
</set>

注意:<one-to-many>元素不需要定义任何字段。 也不需要指定表名。

重要提示:如果一对多关联中的<key>字段定义成NOT NULL,那么当创建和更新关联关系时Hibernate可能引起约束违例。为了预防这个问题,你必须使用双向关联,并且在“多”这一端(Set或者是bag)指明inverse="true"

6.5. 延迟初始化(延迟加载)(Lazy Initialization)

(译者注: 本翻译稿中,对Lazy Initiazation和Eager fetch中的lazy,eager采取意译的方式,分别翻译为延迟初始化和预先抓取。lazt initiazation就是指直到第一次调用时才加载。)

集合(不包括数组)是可以延迟初始化的,意思是仅仅当应用程序需要访问时,才载入他们的值。对于使用者来说,初始化是透明的, 因此应用程序通常不需要关心这个(事实上,透明的延迟加载也就是为什么Hibernate需要自己的集合实现的主要原因)。但是, 如何应用程序试图执行以下程序:

s = sessions.openSession();
User u = (User) s.find("from User u where u.name=?", userName, Hibernate.STRING).get(0);
Map permissions = u.getPermissions();
s.connection().commit();
s.close();

Integer accessLevel = (Integer) permissions.get("accounts");  // Error!

这个错误可能令你感到意外。因为在这个Session被提交(commit)之前, permissions没有被初始化,那么这个集合将永远不能载入他的数据了。 解决方法是把读取集合数据的语句提到Session被提交之前。

另外一种选择是不使用延迟初始化集合。既然延迟初始化可能引起上面这样错误,默认是不使用延迟初始化的。但是, 为了效率的原因, 我们希望对绝大多数集合(特别是实体集合)使用延迟初始化。

延迟初始化集合时发生的例外被封装在LazyInitializationException中。

使用可选的 lazy 属性来定义延迟初始化集合:

<set name="names" table="NAMES" lazy="true">
    <key column="group_id"/>
    <element column="NAME" type="string"/>
</set>

在一些应用程序的体系结构中,特别是使用hibernate访问数据的结构, 代码可能会用在不用的应用层中, 可能没有办法保证当一个集合在初始化的时候, session仍然打开着。 这里有两个基本方法来解决这个问题:

  • 在基于Web的应用程序中, 一个servlet过滤器可以用来在用户请求的完成之前来关闭Session。当然,这个地方(关闭session)严重依赖于你的应用程序结构中例外处理的正确性。在请求返回给用户之前关闭Session和结束事务是非常重要的,即使是在构建视图(译者注: 返回给用户的HTML页面)的时候发生了例外,也必须确保这一点。考虑到这一点,servlet过滤器可以保证能够操作这个Session。我们推荐使用一个ThreadLocal变量来保存当前的Session

  • 在一个有单独的商业层的应用程序中, 商业逻辑必须在返回之前“准备好”Web层所需要的所有集合。通常, 应用程序为每个Web层需要的集合调用Hibernate.initialize()(必须在Session被关闭之前调用)或者通过使用FETCH子句来明确获取到整个集合。

你可以使用Hibernate Session API中的filter() 方法来在初始化之前得到集合的大小:

( (Integer) s.filter( collection, "select count(*)" ).get(0) ).intValue()

filter() 或者 createFilter()同样被用于有效的重新载入一个集合的子集而不需要载入整个集合。

6.6. 集合排序(Sorted Collections)

Hibernate支持实现java.util.SortedMapjava.util.SortedSet的集合。 你必须在映射文件中指定一个比较器:

<set name="aliases" table="person_aliases" sort="natural">
    <key column="person"/>
    <element column="name" type="string"/>
</set>

<map name="holidays" sort="my.custom.HolidayComparator" lazy="true">
    <key column="year_id"/>
    <index column="hol_name" type="string"/>
    <element column="hol_date type="date"/>
</map>

sort属性中允许的值包括unsorted,natural和某个实现了java.util.Comparator的类的名称。

分类集合的行为事实上象java.util.TreeSet或者java.util.TreeMap

6.7. 对collection排序的其他方法(Other Ways To Sort a Collection)

如果你希望数据库自己对集合元素排序,可以利用set,bag或者map映射中的order-by属性。这个解决方案只能在jdk1.4或者更高的jdk版本中才可以实现(通过LinkedHashSet或者 LinkedHashMap实现)。 它是在SQL查询中完成排序,而不是在内存中。

<set name="aliases" table="person_aliases" order-by="name asc">
    <key column="person"/>
    <element column="name" type="string"/>
</set>

<map name="holidays" order-by="hol_date, hol_name" lazy="true">
    <key column="year_id"/>
    <index column="hol_name" type="string"/>
    <element column="hol_date type="date"/>
</map>

注意: 这个order-by属性的值是一个SQL排序子句而不是HQL的!

关联还可以在运行时使用filter()根据任意的条件来排序。

sortedUsers = s.filter( group.getUsers(), "order by this.name" );

6.8. 垃圾收集(Garbage Collection)

集合是在被持久对象引用时自动持久化,并且不再不引用时自动删除的。 如果集合被从一个持久化对象转移到另外一个, 他的数据可能会被从一个表移到另外一个。 你不需要担心这些。 就跟你通常使用java集合一样使用Hibernate集合即可。

注意:上面的论断在inverse="true"的情况下适用。 我们将在接下来的章节中解释这一点。

6.9. 双向关联(Bidirectional Associations)

双向关联允许通过关联的任一端访问另外一端。在Hibernate中, 支持两种类型的双向关联:

一对多(one-to-many)

Set或者bag值在一端, 单独值(非集合)在另外一端

多对多(many-to-many)

两端都是set或bag值

请注意Hibernate不支持带有索引的集合(list,map或者array)作为"多"的那一端的双向one-to-many关联。

要建立一个双向的多对多关联,只需要映射两个many-to-many关联到同一个数据库表中,并再定义其中的一端为inverse。这里有一个从一个类关联到他自身的many-to-many的双向关联的例子: (原文:You may specify a bidirectional many-to-many association simply by mapping two many-to-many associations to the same database table and declaring one end as inverse. Heres an example of a bidirectional many-to-many association from a class back to itself:)

<class name="eg.Node">
    <id name="id" column="id"/>
    ....
    <bag name="accessibleTo" table="node_access" lazy="true">
        <key column="to_node_id"/>
        <many-to-many class="eg.Node" column="from_node_id"/>
    </bag>
     <!-- inverse end -->
    <bag name="accessibleFrom" table="node_access" inverse="true" lazy="true">
        <key column="from_node_id"/>
        <many-to-many class="eg.Node" column="to_node_id"/>
    </bag>
</class>

如果只对关联的反向端进行了改变,这个改变不会被持久化。 (原文:Changes made only to the inverse end of the association are not persisted.)

要建立一个一对多的双向关联,你可以通过把一个一对多关联,作为一个多对一关联映射到到同一张表的字段上,并且在"多"的那一端定义inverse="true"。 (原文: You may map a bidirectional one-to-many association by mapping a one-to-many association to the same table column(s) as a many-to-one association and declaring the many-valued end inverse="true".)

<class name="eg.Parent">
    <id name="id" column="id"/>
    ....
    <set name="children" inverse="true" lazy="true">
        <key column="parent_id"/>
        <one-to-many class="eg.Child"/>
    </set>
</class>

<class name="eg.Child">
    <id name="id" column="id"/>
    ....
    <many-to-one name="parent" class="eg.Parent" column="parent_id"/>
</class>

在“一”这一端定义inverse="true"不会影响级联操作。 (原文:Mapping one end of an association with inverse="true" doesn't affect the operation of cascades.)

6.10. 三重关联(Ternary Associations)

这里有两种可能的途径来映射一个三重关联。其中一个是使用组合元素(下面将讨论).另外一个是使用一个map,并且带有关联作为其索引。

<map name="contracts" lazy="true">
    <key column="employer_id"/>
    <index-many-to-many column="employee_id" class="Employee"/>
    <one-to-many column="contract_id" class="Contract"/>
</map>
<map name="connections" lazy="true">
    <key column="node1_id"/>
    <index-many-to-many column="node2_id" class="Node"/>
    <many-to-many column="connection_id" class="Connection"/>
</map>

6.11. 异类关联(Heterogeneous Associations)

<many-to-any><index-many-to-any>元素提供真正的异类关联。这些元素和<any>元素工作方式是同样的,他们都应该很少用到。

6.12. 集合例子(Collection Example)

在前面的几个章节的确非常令人迷惑。 因此让我们来看一个例子。这个类:

package eg;
import java.util.Set;

public class Parent {
    private long id;
    private Set children;

    public long getId() { return id; }
    private void setId(long id) { this.id=id; }

    private Set getChildren() { return children; }
    private void setChildren(Set children) { this.children=children; }

    ....
    ....
}

这个类有一个eg.Child的实例集合。如果每一个子实例至多有一个父实例, 那么最自然的映射是一个one-to-many的关联关系:

<hibernate-mapping>

    <class name="eg.Parent">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <set name="children" lazy="true">
            <key column="parent_id"/>
            <one-to-many class="eg.Child"/>
        </set>
    </class>

    <class name="eg.Child">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="name"/>
    </class>

</hibernate-mapping>

在以下的表定义中反应了这个映射关系:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null primary key, name varchar(255), parent_id bigint )
alter table child add constraint childfk0 (parent_id) references parent

如果父亲是必须的, 那么就可以使用双向one-to-many的关联了(请看后面父/子关系的章节)。

<hibernate-mapping>

    <class name="eg.Parent">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <set name="children" inverse="true" lazy="true">
            <key column="parent_id"/>
            <one-to-many class="eg.Child"/>
        </set>
    </class>

    <class name="eg.Child">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="name"/>
        <many-to-one name="parent" class="eg.Parent" column="parent_id" not-null="true"/>
    </class>

</hibernate-mapping>

请注意NOT NULL的约束:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null
                     primary key,
                     name varchar(255),
                     parent_id bigint not null )
alter table child add constraint childfk0 (parent_id) references parent

另外一方面,如果一个子实例可能有多个父实例, 那么就应该使用many-to-many关联:

<hibernate-mapping>

    <class name="eg.Parent">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <set name="children" lazy="true" table="childset">
            <key column="parent_id"/>
            <many-to-many class="eg.Child" column="child_id"/>
        </set>
    </class>

    <class name="eg.Child">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="name"/>
    </class>

</hibernate-mapping>

表定义:

create table parent ( id bigint not null primary key )
create table child ( id bigint not null primary key, name varchar(255) )
create table childset ( parent_id bigint not null, child_id bigint not null, primary key ( parent_id, child_id ) )
alter table childset add constraint childsetfk0 (parent_id) references parent
alter table childset add constraint childsetfk1 (child_id) references child

6.13. <idbag>

如果你完全信奉我们对于“联合主键(composite keys)是个坏东西”,和“实体应该使用(无机的)自己生成的代用标识符(surrogate keys)”的观点,也许你会感到有一些奇怪,我们目前为止展示的多对多关联和值集合都是映射成为带有联合主键的表的!现在,这一点非常值得争辩;看上去一个单纯的关联表并不能从代用标识符中获得什么好处(虽然使用组合值的集合可能会获得一点好处)。不过,Hibernate提供了一个(一点点试验性质的)功能,让你把多对多关联和值集合应得到一个使用代用标识符的表去。

<idbag> 属性让你使用bag语义来映射一个List (或Collection)。

<idbag name="lovers" table="LOVERS" lazy="true">
    <collection-id column="ID" type="long">
        <generator class="hilo"/>
    </collection-id>
    <key column="PERSON1"/>
    <many-to-many column="PERSON2" class="eg.Person" outer-join="true"/>
</idbag>

你可以理解,<idbag>人工的id生成器,就好像是实体类一样!集合的每一行都有一个不同的人造关键字。但是,Hibernate没有提供任何机制来让你取得某个特定行的人造关键字。

注意<idbag>的更新性能要比普通的<bag>高得多!Hibernate可以有效的定位到不同的行,分别进行更新或删除工作,就如同处理一个list, map或者set一样。

在目前的实现中,还不支持使用identity标识符生成器策略。(In the current implementation, the identity identifier generation strategy is not supported.)