Hibernate.orgCommunity Documentation

第 6 章 集合映射(Collection mappings)

6.1. 持久化集合类(Persistent collections)
6.2. 集合映射( Collection mappings )
6.2.1. 集合外键(Collection foreign keys)
6.2.2. 集合元素(Collection elements)
6.2.3. 索引集合类(Indexed collections)
6.2.4. 值集合于多对多关联(Collections of values and many-to-many associations)
6.2.5. 一对多关联(One-to-many Associations)
6.3. 高级集合映射(Advanced collection mappings)
6.3.1. 有序集合(Sorted collections)
6.3.2. 双向关联(Bidirectional associations)
6.3.3. 双向关联,涉及有序集合类
6.3.4. 三重关联(Ternary associations)
6.3.5. Using an <idbag>
6.4. 集合例子(Collection example)

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

public class Product {

    private String serialNumber;
    private Set parts = new HashSet();
    
    public Set getParts() { return parts; }
    void setParts(Set parts) { this.parts = parts; }
    public String getSerialNumber() { return serialNumber; }
    void setSerialNumber(String sn) { serialNumber = sn; }
}

实际的接口可能是 java.util.Setjava.util.Collectionjava.util.Listjava.util.Mapjava.util.SortedSetjava.util.SortedMap 或者任何你喜欢的类型("任何你喜欢的类型" 代表你需要编写 org.hibernate.usertype.UserCollectionType 的实现)。

注意我们是如何用一个 HashSet 实例来初始化实例变量的。这是用于初始化新创建(尚未持久化)的类实例中集合值属性的最佳方法。当你持久化这个实例时 — 比如通过调用 persist() — Hibernate 会自动把 HashSet 替换为 Hibernate 自己的 Set 实现。注意下面的错误:

Cat cat = new DomesticCat();

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

根据不同的接口类型,被 Hibernate 注射的持久化集合类的表现类似 HashMapHashSetTreeMapTreeSetArrayList

集合类实例具有值类型的通常行为。当被持久化对象引用后,他们会自动被持久化,当不再被引用后,自动被删除。假若实例被从一个持久化对象传递到另一个,它的元素可能从一个表转移到另一个表。两个实体不能共享同一个集合类实例的引用。因为底层关系数据库模型的原因,集合值属性无法支持空值语义;Hibernate 对空的集合引用和空集合不加区别。

你不需要过多的为此担心。就如同你平时使用普通的 Java 集合类一样来使用持久化集合类。只是要确认你理解了双向关联的语义(后文将进行讨论)。

用于映射集合类的 Hibernate 映射元素取决于接口的类型。比如,<set> 元素用来映射 Set 类型的属性。

<class name="Product">

    <id name="serialNumber" column="productSerialNumber"/>
    <set name="parts">
        <key column="productSerialNumber" not-null="true"/>
        <one-to-many class="Part"/>
    </set>
</class
>

除了 <set>,还有<list><map><bag><array><primitive-array> 映射元素。<map> 具有代表性:

<map
    name="prop(1)ertyName"
    table="tab(2)le_name"
    schema="sc(3)hema_name"
    lazy="true(4)|extra|false"
    inverse="t(5)rue|false"
    cascade="a(6)ll|none|save-update|delete|all-delete-orphan|delete-orphan"
    sort="unso(7)rted|natural|comparatorClass"
    order-by="(8)column_name asc|desc"
    where="arb(9)itrary sql where condition"
    fetch="joi(10)n|select|subselect"
    batch-size(11)="N"
    access="fi(12)eld|property|ClassName"
    optimistic(13)-lock="true|false"
    mutable="t(14)rue|false"
    node="element-name|."
    embed-xml="true|false"
>

    <key .... />
    <map-key .... />
    <element .... />
</map
>

1

name:集合属性的名称

2

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

3

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

4

lazy(可选--默认为 true)可以用来关闭延迟加载(false):指定一直使用预先抓取,或者打开 "extra-lazy" 抓取,此时大多数操作不会初始化集合类(适用于非常大的集合)。

5

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

6

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

7

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

8

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

9

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

10

fetch(可选,默认为 select):用于在外连接抓取、通过后续 select 抓取和通过后续 subselect 抓取之间选择。

11

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

12

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

13

乐观锁(可选 - 默认为 true):对集合的状态的改变会是否导致其所属的实体的版本增长(对一对多关联来说,关闭这个属性常常是有理的)。

14

mutable(可变)(可选 — 默认为 true):若值为 false,表明集合中的元素不会改变(在某些情况下可以进行一些小的性能优化)。

所有的集合映射,除了 set 和 bag 语义的以外,都需要指定一个集合表的索引字段(index column) — 用于对应到数组索引,或者 List 的索引,或者 Map 的关键字。通过 <map-key>Map 的索引可以是任何基础类型;若通过 <map-key-many-to-many>,它也可以是一个实体引用;若通过 <composite-map-key>,它还可以是一个组合类型。数组或列表的索引必须是 integer 类型,并且使用 <list-index> 元素定义映射。被映射的字段包含有顺序排列的整数(默认从 0 开始)。

<list-index
        column(1)="column_name"
        base="(2)0|1|..."/>

1

column_name (required): the name of the column holding the collection index values.

1

base (optional - defaults to 0): the value of the index column that corresponds to the first element of the list or array.

<map-key
        column(1)="column_name"
        formul(2)a="any SQL expression"
        type="(3)type_name"
        node="@attribute-name"
        length="N"/>

1

column (optional): the name of the column holding the collection index values.

2

formula (optional): a SQL formula used to evaluate the key of the map.

3

type (required): the type of the map keys.

<map-key-many-to-many
        column(1)="column_name"
        formul(2)(3)a="any SQL expression"
        class="ClassName"
/>

1

column (optional): the name of the foreign key column for the collection index values.

2

formula (optional): a SQ formula used to evaluate the foreign key of the map key.

3

class (required): the entity class used as the map key.

假若你的表没有一个索引字段,当你仍然希望使用 List 作为属性类型,你应该把此属性映射为 Hibernate <bag>。从数据库中获取的时候,bag 不维护其顺序,但也可选择性的进行排序。

任何值集合或者多对多关联需要专用的具有一个或多个外键字段的 collection table、一个或多个 collection element column,以及还可能有一个或多个索引字段。

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

<element
        column(1)="column_name"
        formul(2)a="any SQL expression"
        type="(3)typename"
        length="L"
        precision="P"
        scale="S"
        not-null="true|false"
        unique="true|false"
        node="element-name"
/>

1

column (optional): the name of the column holding the collection element values.

2

formula (optional): an SQL formula used to evaluate the element.

3

type (required): the type of the collection element.

A many-to-many association is specified using the <many-to-many> element.

<many-to-many
        column(1)="column_name"
        formul(2)a="any SQL expression"
        class=(3)"ClassName"
        fetch=(4)"select|join"
        unique(5)="true|false"
        not-fo(6)und="ignore|exception"
        entity(7)-name="EntityName"
        proper(8)ty-ref="propertyNameFromAssociatedClass"
        node="element-name"
        embed-xml="true|false"
    />

1

column (optional): the name of the element foreign key column.

2

formula (optional): an SQL formula used to evaluate the element foreign key value.

3

class (required): the name of the associated class.

4

fetch (optional - defaults to join): enables outer-join or sequential select fetching for this association. This is a special case; for full eager fetching in a single SELECT of an entity and its many-to-many relationships to other entities, you would enable join fetching,not only of the collection itself, but also with this attribute on the <many-to-many> nested element.

5

unique (optional): enables the DDL generation of a unique constraint for the foreign-key column. This makes the association multiplicity effectively one-to-many.

6

not-found (optional - defaults to exception): specifies how foreign keys that reference missing rows will be handled: ignore will treat a missing row as a null association.

7

entity-name (optional): the entity name of the associated class, as an alternative to class.

8

property-ref (optional): the name of a property of the associated class that is joined to this foreign key. If not specified, the primary key of the associated class is used.

下面是一些例子:

一系列字符串:


<set name="names" table="person_names">
    <key column="person_id"/>
    <element column="person_name" type="string"/>
</set
>

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


<bag name="sizes"
        table="item_sizes" 
        order-by="size asc">
    <key column="item_id"/>
    <element column="size" type="integer"/>
</bag
>

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


<array name="addresses"
        table="PersonAddress" 
        cascade="persist">
    <key column="personId"/>
    <list-index column="sortOrder"/>
    <many-to-many column="addressId" class="Address"/>
</array
>

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


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

一个组件的列表:(将在下一章讨论)


<list name="carComponents"
        table="CarComponents">
    <key column="carId"/>
    <list-index column="sortOrder"/>
    <composite-element class="CarComponent">
        <property name="price"/>
        <property name="type"/>
        <property name="serialNumber" column="serialNum"/>
    </composite-element>
</list
>

一对多关联通过外键连接两个类对应的表,而没有中间集合表。 这个关系模型失去了一些 Java 集合的语义:

一个从 ProductPart 的关联需要关键字字段,可能还有一个索引字段指向 Part 所对应的表。<one-to-many> 标记指明了一个一对多的关联。

<one-to-many
        class=(1)"ClassName"
        not-fo(2)und="ignore|exception"
        entity(3)-name="EntityName"
        node="element-name"
        embed-xml="true|false"
    />

1

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

2

not-found(可选 - 默认为exception):指明若缓存的标示值关联的行缺失,该如何处理:ignore 会把缺失的行作为一个空关联处理。

3

entity-name(可选):被关联的类的实体名,作为 class 的替代。

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

下面的例子展示一个 Part 实体的 map,把 name 作为关键字。( partNamePart 的持久化属性)。注意其中的基于公式的索引的用法。


<map name="parts"
        cascade="all">
    <key column="productId" not-null="true"/>
    <map-key formula="partName"/>
    <one-to-many class="Part"/>
</map
>

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">
    <key column="year_id"/>
    <map-key column="hol_name" type="string"/>
    <element column="hol_date" type="date"/>
</map
>

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

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

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


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

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

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

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

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

要建立一个双向的多对多关联,只需要映射两个 many-to-many 关联到同一个数据库表中,并再定义其中的一端为 inverse(使用哪一端要根据你的选择,但它不能是一个索引集合)。

这里有一个 many-to-many 的双向关联的例子;每一个 category 都可以有很多 items,每一个 items 可以属于很多 categories:


<class name="Category">
    <id name="id" column="CATEGORY_ID"/>
    ...
    <bag name="items" table="CATEGORY_ITEM">
        <key column="CATEGORY_ID"/>
        <many-to-many class="Item" column="ITEM_ID"/>
    </bag>
</class>

<class name="Item">
    <id name="id" column="ITEM_ID"/>
    ...

    <!-- inverse end -->
    <bag name="categories" table="CATEGORY_ITEM" inverse="true">
        <key column="ITEM_ID"/>
        <many-to-many class="Category" column="CATEGORY_ID"/>
    </bag>
</class
>

如果只对关联的反向端进行了改变,这个改变不会被持久化。 这表示 Hibernate 为每个双向关联在内存中存在两次表现,一个从 A 连接到 B,另一个从 B 连接到 A。如果你回想一下 Java 对象模型,我们是如何在 Java 中创建多对多关系的,这可以让你更容易理解:



category.getItems().add(item);          // The category now "knows" about the relationship
item.getCategories().add(category);     // The item now "knows" about the relationship
session.persist(item);                   // The relationship won't be saved!
session.persist(category);               // The relationship will be saved

非反向端用于把内存中的表示保存到数据库中。

要建立一个一对多的双向关联,你可以通过把一个一对多关联,作为一个多对一关联映射到到同一张表的字段上,并且在"多"的那一端定义 inverse="true"


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

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

在“一”这一端定义 inverse="true" 不会影响级联操作,二者是正交的概念。

对于有一端是 <list> 或者 <map> 的双向关联,需要加以特别考虑。假若子类中的一个属性映射到索引字段,没问题,我们仍然可以在集合类映射上使用 inverse="true"


<class name="Parent">
    <id name="id" column="parent_id"/>
    ....
    <map name="children" inverse="true">
        <key column="parent_id"/>
        <map-key column="name" 
            type="string"/>
        <one-to-many class="Child"/>
    </map>
</class>

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

但是,假若子类中没有这样的属性存在,我们不能认为这个关联是真正的双向关联(信息不对称,在关联的一端有一些另外一端没有的信息)。在这种情况下,我们不能使用 inverse="true"。我们需要这样用:


<class name="Parent">
    <id name="id" column="parent_id"/>
    ....
    <map name="children">
        <key column="parent_id"
            not-null="true"/>
        <map-key column="name" 
            type="string"/>
        <one-to-many class="Child"/>
    </map>
</class>

<class name="Child">
    <id name="id" column="child_id"/>
    ....
    <many-to-one name="parent" 
        class="Parent" 
        column="parent_id"
        insert="false"
        update="false"
        not-null="true"/>
</class
>

注意在这个映射中,关联中集合类"值"一端负责来更新外键。

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

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


<idbag name="lovers" table="LOVERS">
    <collection-id column="ID" type="long">
        <generator class="sequence"/>
    </collection-id>
    <key column="PERSON1"/>
    <many-to-many column="PERSON2" class="Person" fetch="join"/>
</idbag
>

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

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

在目前的实现中,还不支持使用 identity 标识符生成器策略来生成 <idbag> 集合的标识符。

集合例子(Collection example)。

下面的代码是用来添加一个新的 Child

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; }
    ....
    ....
}

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


<hibernate-mapping>

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

    <class name="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="Parent">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <set name="children" inverse="true">
            <key column="parent_id"/>
            <one-to-many class="Child"/>
        </set>
    </class>

    <class name="Child">
        <id name="id">
            <generator class="sequence"/>
        </id>
        <property name="name"/>
        <many-to-one name="parent" class="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

另外,如果你绝对坚持这个关联应该是单向的,你可以对 <key> 映射声明 NOT NULL 约束:


<hibernate-mapping>

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

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

</hibernate-mapping
>

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


<hibernate-mapping>

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

    <class name="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

For more examples and a complete explanation of a parent/child relationship mapping, see 第 22 章 示例:父子关系(Parent/Child) for more information.

甚至可能出现更加复杂的关联映射,我们会在下一章中列出所有可能性。