Hibernate.orgCommunity Documentation
(译者注:在阅读本章的时候,以后整个手册的阅读过程中,我们都会面临一个名词方面的问题,那就是“集合”。"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.Set
、java.util.Collection
、java.util.List
、java.util.Map
、java.util.SortedSet
、java.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 注射的持久化集合类的表现类似 HashMap
、HashSet
、TreeMap
、TreeSet
或 ArrayList
。
集合类实例具有值类型的通常行为。当被持久化对象引用后,他们会自动被持久化,当不再被引用后,自动被删除。假若实例被从一个持久化对象传递到另一个,它的元素可能从一个表转移到另一个表。两个实体不能共享同一个集合类实例的引用。因为底层关系数据库模型的原因,集合值属性无法支持空值语义;Hibernate 对空的集合引用和空集合不加区别。
你不需要过多的为此担心。就如同你平时使用普通的 Java 集合类一样来使用持久化集合类。只是要确认你理解了双向关联的语义(后文将进行讨论)。
从集合类可以产生很大一部分映射,覆盖了很多常见的关系模型。我们建议你试验 schema 生成工具,来体会一下不同的映射声明是如何被翻译为数据库表的。
用于映射集合类的 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="propertyName" table="tab
le_name" schema="sc
hema_name" lazy="true
|extra|false" inverse="t
rue|false" cascade="a
ll|none|save-update|delete|all-delete-orphan|delete-orphan" sort="unso
rted|natural|comparatorClass" order-by="
column_name asc|desc" where="arb
itrary sql where condition" fetch="joi
n|select|subselect" batch-size
="N" access="fi
eld|property|ClassName" optimistic
-lock="true|false" mutable="t
rue|false" node="element-name|." embed-xml="true|false" > <key .... /> <map-key .... /> <element .... /> </map >
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
集合实例在数据库中依靠持有集合的实体的外键加以辨别。此外键作为集合关键字段(collection key column)(或多个字段)加以引用。集合关键字段通过 <key>
元素映射。
在外键字段上可能具有非空约束。对于大多数集合来说,这是隐含的。对单向一对多关联来说,外键字段默认是可以为空的,因此你可能需要指明 not-null="true"
。
<key column="productSerialNumber" not-null="true"/>
外键约束可以使用 ON DELETE CASCADE
。
<key column="productSerialNumber" on-delete="cascade"/>
对 <key>
元素的完整定义,请参阅前面的章节。
集合几乎可以包含任何其他的 Hibernate 类型,包括所有的基本类型、自定义类型、组件,当然还有对其他实体的引用。存在一个重要的区别:位于集合中的对象可能是根据“值”语义来操作(其声明周期完全依赖于集合持有者),或者它可能是指向另一个实体的引用,具有其自己的生命周期。在后者的情况下,被作为集合持有的状态考虑的,只有两个对象之间的“连接”。
被包容的类型被称为集合元素类型(collection element type)。集合元素通过 <element>
或 <composite-element>
映射,或在其是实体引用的时候,通过 <one-to-many>
或 <many-to-many>
映射。前两种用于使用值语义映射元素,后两种用于映射实体关联。
所有的集合映射,除了 set 和 bag 语义的以外,都需要指定一个集合表的索引字段(index column) — 用于对应到数组索引,或者 List
的索引,或者 Map
的关键字。通过 <map-key>
,Map
的索引可以是任何基础类型;若通过 <map-key-many-to-many>
,它也可以是一个实体引用;若通过 <composite-map-key>
,它还可以是一个组合类型。数组或列表的索引必须是 integer
类型,并且使用 <list-index>
元素定义映射。被映射的字段包含有顺序排列的整数(默认从 0 开始)。
<list-index column="column_name" base="
0|1|..."/>
| |
|
<map-key column="column_name" formul
a="any SQL expression" type="
type_name" node="@attribute-name" length="N"/>
| |
| |
|
<map-key-many-to-many column="column_name" formul
a="any SQL expression" class="ClassName" />
| |
| |
|
假若你的表没有一个索引字段,当你仍然希望使用 List
作为属性类型,你应该把此属性映射为 Hibernate <bag>。从数据库中获取的时候,bag 不维护其顺序,但也可选择性的进行排序。
任何值集合或者多对多关联需要专用的具有一个或多个外键字段的 collection table、一个或多个 collection element column,以及还可能有一个或多个索引字段。
对于一个值集合,我们使用 <element>
标签。例如:
<element column="column_name" formul
a="any SQL expression" type="
typename" length="L" precision="P" scale="S" not-null="true|false" unique="true|false" node="element-name" />
| |
| |
|
A many-to-many association is specified using the <many-to-many>
element.
<many-to-many column="column_name" formul
a="any SQL expression" class=
"ClassName" fetch=
"select|join" unique
="true|false" not-fo
und="ignore|exception" entity
-name="EntityName" proper
ty-ref="propertyNameFromAssociatedClass" node="element-name" embed-xml="true|false" />
| |
| |
| |
| |
| |
| |
| |
|
下面是一些例子:
一系列字符串:
<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 集合的语义:
一个被包含的实体的实例只能被包含在一个集合的实例中。
一个被包含的实体的实例只能对应于集合索引的一个值中。
一个从 Product
到 Part
的关联需要关键字字段,可能还有一个索引字段指向 Part
所对应的表。<one-to-many>
标记指明了一个一对多的关联。
<one-to-many class="ClassName" not-fo
und="ignore|exception" entity
-name="EntityName" node="element-name" embed-xml="true|false" />
| |
| |
|
注意:<one-to-many>
元素不需要定义任何字段。也不需要指定表名。
重要提示:如果一对多
关联中的外键字段定义成 NOT NULL
,你必须把 <key>
映射声明为 not-null="true"
,或者使用双向关联,并且标明 inverse="true"
。参阅本章后面关于双向关联的讨论。
下面的例子展示一个 Part
实体的 map,把 name 作为关键字。( partName
是 Part
的持久化属性)。注意其中的基于公式的索引的用法。
<map name="parts"
cascade="all">
<key column="productId" not-null="true"/>
<map-key formula="partName"/>
<one-to-many class="Part"/>
</map
>
Hibernate 支持实现 java.util.SortedMap
和 java.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
属性中允许的值包括 unsorted
,natural
和某个实现了 java.util.Comparator
的类的名称。
分类集合的行为事实上象 java.util.TreeSet
或者 java.util.TreeMap
。
如果你希望数据库自己对集合元素排序,可以利用 set
,bag
或者 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
>
注意:这个 order-by
属性的值是一个 SQL 排序子句而不是 HQL 的。
关联还可以在运行时使用集合 filter()
根据任意的条件来排序:
sortedUsers = s.createFilter( group.getUsers(), "order by this.name" ).list();
双向关联允许通过关联的任一端访问另外一端。在 Hibernate 中,支持两种类型的双向关联:
Set 或者 bag 值在一端,单独值(非集合)在另外一端
两端都是 set 或 bag 值
要建立一个双向的多对多关联,只需要映射两个 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
>
注意在这个映射中,关联中集合类"值"一端负责来更新外键。
有三种可能的途径来映射一个三重关联。第一种是使用一个 Map
,把一个关联作为其索引:
<map name="contracts">
<key column="employer_id" not-null="true"/>
<map-key-many-to-many column="employee_id" class="Employee"/>
<one-to-many class="Contract"/>
</map
>
<map name="connections">
<key column="incoming_node_id"/>
<map-key-many-to-many column="outgoing_node_id" class="Node"/>
<many-to-many column="connection_id" class="Connection"/>
</map
>
第二种方法是简单的把关联重新建模为一个实体类。这使我们最经常使用的方法。
最后一种选择是使用复合元素,我们会在后面讨论。
如果你完全信奉我们对于“联合主键(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.
甚至可能出现更加复杂的关联映射,我们会在下一章中列出所有可能性。
版权 © 2004 Red Hat, Inc.