Hibernate关联关系映射
1. 介绍
关联关系映射通常情况是最难配置正确的。在这个部分中,我们从单向关系映射开始,然后考虑双向关系映射,逐步讲解典型的案例。在所有的例子中,我们都使将用 Person 和 Address。我们根据映射关系是否涉及连接表以及多样性(multiplicity)来划分关联类型。
在传统的数据建模中,允许为 Null 值的外键被认为是一种不好的实践,因此我们所有的例子中都使用不允许为 Null 的外键。这并不是 Hibernate的 要求,即使你删除掉不允许为 Null 的约束,Hibernate 映射一样可以工作的很好。
2. 单向关联(Unidirectional associations)
2.1. 多对一(many-to-one)
单向 many-to-one 关联是最常见的单向关联关系。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <many-to-one name="address" column="addressId" not-null="true"/> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
输出:
create table Person ( personId bigint not null primary key, addressId bigint not null ) create table Address ( addressId bigint not null primary key )
2.2. 一对一(One-to-one)
基于外键关联的单向一对一关联和单向多对一关联几乎是一样的。唯一的不同就是单向一对一关联中的外键字段具有唯一性约束。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <many-to-one name="address" column="addressId" unique="true" not-null="true"/> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null unique ) create table Address ( addressId bigint not null primary key )
基于主键关联的单向一对一关联通常使用一个特定的 id 生成器,然而在这个例子中我们掉换了关联的方向:
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> </class> <class name="Address"> <id name="id" column="personId"> <generator class="foreign"> <param name="property">person</param> </generator> </id> <one-to-one name="person" constrained="true"/> </class>
create table Person ( personId bigint not null primary key ) create table Address ( personId bigint not null primary key )
2.3. 一对多(one-to-many)
基于外键关联的单向一对多关联是一种很少见的情况,我们不推荐使用它。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <set name="addresses"> <key column="personId" not-null="true"/> <one-to-many class="Address"/> </set> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key ) create table Address ( addressId bigint not null primary key, personId bigint not null )
我们认为对于这种关联关系最好使用连接表。
3. 使用连接表的单向关联(Unidirectional associations with join tables)
3.1. 一对多(one-to-many)
基于连接表的单向一对多关联 应该优先被采用。请注意,通过指定unique="true",我们可以把多样性从多对多改变为一对多。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <set name="addresses" table="PersonAddress"> <key column="personId"/> <many-to-many column="addressId" unique="true" class="Address"/> </set> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId not null, addressId bigint not null primary key ) create table Address ( addressId bigint not null primary key )
3.2. 多对一(many-to-one)
基于连接表的单向多对一关联在关联关系可选的情况下应用也很普遍。例如:
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <join table="PersonAddress" optional="true"> <key column="personId" unique="true"/> <many-to-one name="address" column="addressId" not-null="true"/> </join> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null primary key, addressId bigint not null ) create table Address ( addressId bigint not null primary key )
3.3. 一对一(One-to-one)
基于连接表的单向一对一关联也是可行的,但非常少见。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <join table="PersonAddress" optional="true"> <key column="personId" unique="true"/> <many-to-one name="address" column="addressId" not-null="true" unique="true"/> </join> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique ) create table Address ( addressId bigint not null primary key )
3.4. 多对多(many-to-many)
最后,这里是一个单向多对多关联的例子。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <set name="addresses" table="PersonAddress"> <key column="personId"/> <many-to-many column="addressId" class="Address"/> </set> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) ) create table Address ( addressId bigint not null primary key )
4. 双向关联(Bidirectional associations)
4.1. 一对多(one to many)/多对一(many to one)
双向多对一关联 是最常见的关联关系。下面的例子解释了这种标准的父/子关联关系。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <many-to-one name="address" column="addressId" not-null="true"/> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <set name="people" inverse="true"> <key column="addressId"/> <one-to-many class="Person"/> </set> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null ) create table Address ( addressId bigint not null primary key )
如果你使用 List(或者其他有序集合类),你需要设置外键对应的 key 列为 not null。Hibernate 将从集合端管理关联,维护每个元素的索引,并通过设置 update="false" 和 insert="false" 来对另一端反向操作。
<class name="Person"> <id name="id"/> ... <many-to-one name="address" column="addressId" not-null="true" insert="false" update="false"/> </class> <class name="Address"> <id name="id"/> ... <list name="people"> <key column="addressId" not-null="true"/> <list-index column="peopleIdx"/> <one-to-many class="Person"/> </list> </class>
假若集合映射的 <key> 元素对应的底层外键字段是 NOT NULL 的,那么为这一 key 元素定义 not-null="true" 是很重要的。不要仅仅为可能的嵌套 <column>元素定义 not-null="true",<key> 元素也是需要的。
4.2. 一对一(One-to-one)
基于外键关联的双向一对一关联也很常见。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <many-to-one name="address" column="addressId" unique="true" not-null="true"/> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <one-to-one name="person" property-ref="address"/> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null unique ) create table Address ( addressId bigint not null primary key )
基于主键关联的一对一关联需要使用特定的 id 生成器:
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <one-to-one name="address"/> </class> <class name="Address"> <id name="id" column="personId"> <generator class="foreign"> <param name="property">person</param> </generator> </id> <one-to-one name="person" constrained="true"/> </class>
create table Person ( personId bigint not null primary key ) create table Address ( personId bigint not null primary key )
5. 使用连接表的双向关联(Bidirectional associations with join tables)
5.1. 一对多(one to many)/多对一(many to one)
下面是一个基于连接表的双向一对多关联的例子。注意 inverse="true" 可以出现在关联的任意一端,即 collection 端或者 join 端。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <set name="addresses" table="PersonAddress"> <key column="personId"/> <many-to-many column="addressId" unique="true" class="Address"/> </set> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <join table="PersonAddress" inverse="true" optional="true"> <key column="addressId"/> <many-to-one name="person" column="personId" not-null="true"/> </join> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null, addressId bigint not null primary key ) create table Address ( addressId bigint not null primary key )
5.2. 一对一(one to one)
基于连接表的双向一对一关联也是可行的,但极为罕见。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <join table="PersonAddress" optional="true"> <key column="personId" unique="true"/> <many-to-one name="address" column="addressId" not-null="true" unique="true"/> </join> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <join table="PersonAddress" optional="true" inverse="true"> <key column="addressId" unique="true"/> <many-to-one name="person" column="personId" not-null="true" unique="true"/> </join> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique ) create table Address ( addressId bigint not null primary key )
5.3. 多对多(many-to-many)
下面是一个双向多对多关联的例子。
<class name="Person"> <id name="id" column="personId"> <generator class="native"/> </id> <set name="addresses" table="PersonAddress"> <key column="personId"/> <many-to-many column="addressId" class="Address"/> </set> </class> <class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <set name="people" inverse="true" table="PersonAddress"> <key column="addressId"/> <many-to-many column="personId" class="Person"/> </set> </class>
create table Person ( personId bigint not null primary key ) create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) ) create table Address ( addressId bigint not null primary key )
6. 更复杂的关联映射
更复杂的关联连接极为罕见。通过在映射文档中嵌入 SQL 片断,Hibernate 也可以处理更为复杂的情况。比如,假若包含历史帐户数据的表定义了 accountNumber、effectiveEndDate 和 effectiveStartDate 字段,按照下面映射:
<properties name="currentAccountKey"> <property name="accountNumber" type="string" not-null="true"/> <property name="currentAccount" type="boolean"> <formula>case when effectiveEndDate is null then 1 else 0 end</formula> </property> </properties> <property name="effectiveEndDate" type="date"/> <property name="effectiveStateDate" type="date" not-null="true"/>
那么我们可以对目前(current)实例(其 effectiveEndDate 为 null)使用这样的关联映射:
<many-to-one name="currentAccountInfo" property-ref="currentAccountKey" class="AccountInfo"> <column name="accountNumber"/> <formula>'1'</formula> </many-to-one>
在更复杂的例子中,假想 Employee 和 Organization 之间的关联是通过一个 Employment 中间表维护的,而中间表中填充了很多历史雇员数据。那“雇员的最新雇主”这个关联(最新雇主就是具有最新的 startDate 的那个)可以这样映射:
<join> <key column="employeeId"/> <subselect> select employeeId, orgId from Employments group by orgId having startDate = max(startDate) </subselect> <many-to-one name="mostRecentEmployer" class="Organization" column="orgId"/> </join>
使用这一功能时可以充满创意和灵活性,但通常更加实用的是用 HQL 或条件查询来处理这些情况。