一对多关系在系统实现中也很常见。典型的例子就是父亲与孩子的关系。 而在我们现在的这个示例中,每个用户(TUser)都关联到多个地址(TAddress),如一个用户可能拥有办公室地址、家庭地址等多个地址属性。这样,在系统中,就反应为一个“一对多”关联。
一对多关系分为单向一对多关系和双向一对多关系。
单向一对多关系只需在“一”方进行配置,双向一对多关系需要在关联双方均加以配置。
Ø 单向一对多关系
配置:
对于主控方(TUser):
TUser.hbm.xml:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
>
……
<set
name="addresses"
table="t_address"
lazy="false"
inverse="false"
cascade="all"
sort="unsorted"
order-by="zipcode asc"
>
<key
column="user_id"
>
</key>
<one-to-many
class="org.hibernate.sample.TAddress"
/>
</set>
……
</class>
</hibernate-mapping>
被动方(Taddress)的记录由Hibernate 负责读取,之后存放在主控方指定的Collection类型属性中。
对于one-to-many 关联关系, 我们可以采用java.util.Set ( 或者net.sf.hibernate.collection.Bag)类型的Collection,表现在XML 映射文件中也就是<set>…</set>(或<bag>…</bag>)节点。关于Hibernate的Collection实现,请参见Hibernate Reference.
one-to-many 节点有以下属性:
属性 | 描述 | 类型 | 必须 |
name | 映射属性 | Text | Y |
table | 目标关联数据库表。 | Text | Y |
lazy | 是否采用延迟加载。 | Text | N |
inverse | 用于标识双向关联中的被动方一端。 inverse=false的一方(主控方)负责 维护关联关系。 默认值: false | Bool | N |
cascade | 操作级联(cascade)关系。 可选值: all : 所有情况下均进行级联操作。 none:所有情况下均不进行级联操作。 save-update:在执行save-update时 进行级联操作。 delete:在执行delete时进行级联操作。 | Text | N |
sort | 排序类型。 可选值: unsorted :不排序(默认) natural :自然顺序(避免与order-by 搭配使用) comparatorClass :指以某个实现了 java.util.Comparator接口的类作为排 序算法。 | Text | N |
order-by | 指定排序字段及其排序方式。 (JDK1.4以上版本有效)。 对应SQL中的order by子句。 避免与sort 的 “natural”模式同时使 用。 | Text | N |
where | 数据甄选条件,如果只需要处理库表中某 些特定数据的时候,可通过此选项设定结 果集限定条件。 | Text | N |
outer-join | 是否使用外联接。 true:总是使用outer-join false:不使用outer-join auto(默认) :如果关联对象没有采用 Proxy机制,则使用outer-join. | Text | N |
batch-size | 采用延迟加载特性时(Lazy Loading) 一次读入的数据数量。 此处未采用延迟加载机制,因此此属性忽 略。 | Int | N |
access | 属性值的读取方式。 可选项: field property(默认) ClassName | Text | N |
通过单向一对多关系进行关联相对简单,但是存在一个问题。由于是单向关联,为了保持关联关系,我们只能通过主控方对被动方进行级联更新。且如果被关联方的关联字段为“NOT NULL”,当Hibernate创建或者更新关联关系时,还可能出现约束违例。
例如我们想为一个已有的用户“Erica”添加一个地址对象:
Transaction tx = session.beginTransaction();
TAddress addr = new TAddress();
Hibernate Developer’s Guide Version 1.0
September 2, 2004 So many open source projects. Why not Open your Documents?
addr.setTel("1123");
addr.setZipcode("233123");
addr.setAddress("Hongkong");
user.getAddresses().add(addr);
session.save(user);//通过主控对象级联更新
tx.commit();
为了完成这个操作,Hibernate会分两步(两条SQL)来完成新增t_address记录的操作:
1. save(user)时:
insert into t_address (user_id, address, zipcode, tel)
values (null, "Hongkong", "233123", "1123")
2. tx.commit()时
update t_address set user_id=”1”, address="Hongkong",
zipcode="233123", tel="1123" where id=2
第一条SQL用于插入新的地址记录。
第二条SQL用于更新t_address,将user_id设置为其关联的user对象的id值。问题就出在这里,数据库中,我们的t_address.user_id字段为“NOT NULL”型,当Hibernate执行第一条语句创建t_address记录时,试图将user_id字段的值设为null,于是引发了一个约束违例异常:
net.sf.hibernate.PropertyValueException: not-null property
references a null or transient value:
org.hibernate.sample.TAddress.userId
因为关联方向是单向,关联关系由TUser对象维持,而被关联的addr对象本身并不知道自己与哪个TUser对象相关联,也就是说,addr对象本身并不知道user_id应该设为什么数值。
因此,在保存addr时,只能先在关联字段插入一个空值。之后,再由TUser对象将自身的id值赋予关联字段addr.user_id,这个赋值操作导致addr对象属性发生变动,在事务提交时,hibernate会发现这一改变,并通过update sql将变动后的数据保存到数据库。
第一个步骤中,企图向数据库的非空字段插入空值,因此导致了约束违例。
既然TUser对象是主控方,为什么就不能自动先设置好下面的TAddress对象的关俩字段值再一次做Insert操作呢?莫名其妙?Ha,don’t ask me ,go to ask
Hibernate TeamJ。
Hibernate Developer’s Guide Version 1.0
September 2, 2004 So many open source projects. Why not Open your Documents?
我们可以在设计的时候通过一些手段进行调整,以避免这样的约束违例,如将关联字段设为允许NULL值、直接采用数值型字段作为关联(有的时候这样的调整并不可行,很多情况下我们必须针对现有数据库结构进行开发),或者手动为关联字段属性
赋一个任意非空值(即使在这里通过手工设置了正确的user_id也没有意义,hibernate还是会自动再调用一条Update语句进行更新)。
甚至我们可以将被动方的关联字段从其映射文件中剔除(如将user_id字段的映射从TAddress.hbm.xml中剔除)。
这样Hibernate在生成第一条insert语句的时候就不会包含这个字段(数据库会使用字段默认值填充),如:之后update语句会根据主控方的one-to-many映射配置中的关联字段去更新被动方关联字段的内容。在我们这里的例子中,如果将user_id字段从TAddress.hbm.xml文件中剔除,Hibernate在保存数据时会生成下面几条SQL:
1. insert into t_address (address, zipcode, tel) values
('Hongkong', '233123', '1123')
2. update t_address set user_id=1 where id=7
生成第一条insert语句时,没有包含user_id字段,数据库会使用该字段的默认值(如果有的话)进行填充。因此不会引发约束违例。之后,根据第一条语句返回的记录id,再通过update语句对user_id字段进行更新。
但是,纵使采用这些权益之计,由于Hibernate实现机制中,采用了两条SQL进行一次数据插入操作,相对单条insert,几乎是两倍的性能开销,效率较低,因此,对于性能敏感的系统而言,这样的解决方案所带来的开销可能难以承受。
针对上面的情况,我们想到,如果addr对象知道如何获取user_id字段的内容,那么执行insert语句的时候直接将数据植入即可。这样不但绕开了约束违例的可能,而且还节省了一条Update语句的开销,大幅度提高了性能。
双向一对多关系的出现则解决了这个问题。它除了避免约束违例和提高性能的好处之外,还带来另外一个优点,由于建立了双向关联,我们可以在关联双方中任意一方,访问关联的另一方(如可以通过TAddress对象直接访问其关联的TUser对象),这提供了更丰富灵活的控制手段。