inverse 和 Cascading 在对象关联时,非常重要,官方文档把它分散在几个地方讲解。
下面搜集了多方文章,集中讨论这个问题:
重点是理解:UML中的多向性、导向性与inverse ,cascading之间的关系;以及inverse,cascading对集合行为的影响力。
一、引用Huihoo翻译Hibernate官方文档 2.3.
第二部分 - 关联映射
我们已经映射了一个持久化实体类到一个表上。让我们在这个基础上增加一些类之间的关联性。
首先我们往我们程序里面增加人(people)的概念,并存储他们所参与的一个Event列表。
(译者注:与Event一样,我们在后面的教程中将直接使用person来表示“人”而不是它的中文翻译)
最初的Person类是简单的:
public class Person {
private Long id;
private int age;
private String firstname;
private String lastname;
Person() {}
// Accessor methods for all properties, private setter for 'id'
}
Create a new mapping file called Person.hbm.xml:
<hibernate-mapping>
<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>
<property name="lastname"/>
</class>
</hibernate-mapping>
Finally, add the new mapping to Hibernate's configuration:
<mapping resource="Event.hbm.xml"/>
<mapping resource="Person.hbm.xml"/>
我们现在将在这两个实体类之间创建一个关联。显然,person可以参与一系列Event,而Event也有不同的参加者(person)。
设计上面我们需要考虑的问题是关联的方向(directionality),阶数(multiplicity)和集合(collection)的行为。
我们将向Person类增加一组Event。这样我们可以轻松的通过调用aPerson.getEvents()
得到一个Person所参与的Event列表,而不必执行一个显式的查询。我们使用一个Java的集合类:一个Set,因为Set
不允许包括重复的元素而且排序和我们无关。
目前为止我们设计了一个单向的,在一端有许多值与之对应的关联,通过Set来实现。
让我们为这个在Java类里编码并映射这个关联:
public class Person {
private Set events = new HashSet();
public Set getEvents() {
return events;
}
public void setEvents(Set events) {
this.events = events;
}
}
在我们映射这个关联之前,先考虑这个关联另外一端。很显然的,我们可以保持这个关联是单向的。如果我们希望这个关联是双向的,
我们可以在Event里创建另外一个集合,例如:anEvent.getParticipants()。
这是留给你的一个设计选项,但是从这个讨论中我们可以很清楚的了解什么是关联的阶数(multiplicity):在这个关联的两端都是“多”。
我们叫这个为:多对多(many-to-many)关联。因此,我们使用Hibernate的many-to-many映射:
<class name="Person" table="PERSON">
<id name="id" column="PERSON_ID">
<generator class="increment"/>
</id>
<property name="age"/>
<property name="firstname"/>
<property name="lastname"/>
<set name="events" table="PERSON_EVENT">
<key column="PERSON_ID"/>
<many-to-many column="EVENT_ID" class="Event"/>
</set>
</class>
Hibernate支持所有种类的集合映射,<set>是最普遍被使用的。对于多对多(many-to-many)关联(或者叫n:m实体关系),
需要一个用来储存关联的表(association table)。表里面的每一行代表从一个person到一个event的一个关联。
表名是由set元素的table属性值配置的。关联里面的标识字段名,person的一端,是
由<key>元素定义,event一端的字段名是由<many-to-many>元素的
column属性定义的。你也必须告诉Hibernate集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。
这个映射的数据库表定义如下:
_____________ __________________
| | | | _____________
| EVENTS | | PERSON_EVENT | | |
|_____________| |__________________| | PERSON |
| | | | |_____________|
| *EVENT_ID | <--> | *EVENT_ID | | |
| EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID |
| TITLE | |__________________| | AGE |
|_____________| | FIRSTNAME |
| LASTNAME |
|_____________|
让我们把一些people和event放到EventManager的一个新方法中:
private void addPersonToEvent(Long personId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
Person aPerson = (Person) session.load(Person.class, personId);
Event anEvent = (Event) session.load(Event.class, eventId);
aPerson.getEvents().add(anEvent);
tx.commit();
HibernateUtil.closeSession();
}
在加载一个Person和一个Event之后,简单的使用普通的方法修改集合。
如你所见,没有显式的update()或者save(), Hibernate自动检测到集合已经被修改
并需要保存。这个叫做automatic dirty checking,你也可以尝试修改任何对象的name或者date的参数。
只要他们处于persistent状态,也就是被绑定在某个Hibernate Session上(例如:他们
刚刚在一个单元操作从被加载或者保存),Hibernate监视任何改变并在后台隐式执行SQL。同步内存状态和数据库的过程,通常只在
一个单元操作结束的时候发生,这个过程被叫做flushing。
你当然也可以在不同的单元操作里面加载person和event。或者在一个Session以外修改一个
不是处在持久化(persistent)状态下的对象(如果该对象以前曾经被持久化,我们称这个状态为脱管(detached))。
在程序里,看起来像下面这样:
private void addPersonToEvent(Long personId, Long eventId) {
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
Person aPerson = (Person) session.load(Person.class, personId);
Event anEvent = (Event) session.load(Event.class, eventId);
tx.commit();
HibernateUtil.closeSession();
aPerson.getEvents().add(anEvent); // aPerson is detached
Session session2 = HibernateUtil.currentSession();
Transaction tx2 = session.beginTransaction();
session2.update(aPerson); // Reattachment of aPerson
tx2.commit();
HibernateUtil.closeSession();
}
对update的调用使一个脱管对象(detached object)重新持久化,你可以说它被绑定到
一个新的单元操作上,所以任何你对它在脱管(detached)状态下所做的修改都会被保存到数据库里。
这个对我们当前的情形不是很有用,但是它是非常重要的概念,你可以把它设计进你自己的程序中。现在,加进一个新的
选项到EventManager的main方法中,并从命令行运行它来完成这个练习。如果你需要一个person和
一个event的标识符 - save()返回它。*******这最后一句看不明白
上面是一个关于两个同等地位的类间关联的例子,这是在两个实体之间。像前面所提到的那样,也存在其它的特别的类和类型,这些类和类型通常是“次要的”。
其中一些你已经看到过,好像int或者String。我们称呼这些类为值类型(value type),
它们的实例依赖(depend)在某个特定的实体上。这些类型的实例没有自己的身份(identity),也不能在实体间共享
(比如两个person不能引用同一个firstname对象,即使他们有相同的名字)。当然,value types并不仅仅在JDK中存在
(事实上,在一个Hibernate程序中,所有的JDK类都被视为值类型),你也可以写你自己的依赖类,例如Address,
MonetaryAmount。
你也可以设计一个值类型的集合(collection of value types),这个在概念上与实体的集合有很大的不同,但是在Java里面看起来几乎是一样的。
我们把一个值类型对象的集合加入Person。我们希望保存email地址,所以我们使用String,
而这次的集合类型又是Set:
private Set emailAddresses = new HashSet();
public Set getEmailAddresses() {
return emailAddresses;
}
public void setEmailAddresses(Set emailAddresses) {
this.emailAddresses = emailAddresses;
}
Set的映射
<set name="emailAddresses" table="PERSON_EMAIL_ADDR">
<key column="PERSON_ID"/>
<element type="string" column="EMAIL_ADDR"/>
</set>
比较这次和较早先的映射,差别主要在element部分这次并没有包括对其它实体类型的引用,而是使用一个元素类型是
String的集合(这里使用小写的名字是向你表明它是一个Hibernate的映射类型或者类型转换器)。
和以前一样,set的table参数决定用于集合的数据库表名。key元素
定义了在集合表中使用的外键。element元素的column参数定义实际保存String值
的字段名。
看一下修改后的数据库表定义。
_____________ __________________
| | | | _____________
| EVENTS | | PERSON_EVENT | | | ___________________
|_____________| |__________________| | PERSON | | |
| | | | |_____________| | PERSON_EMAIL_ADDR |
| *EVENT_ID | <--> | *EVENT_ID | | | |___________________|
| EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID |
| TITLE | |__________________| | AGE | | *EMAIL_ADDR |
|_____________| | FIRSTNAME | |___________________|
| LASTNAME |
|_____________|
你可以看到集合表(collection table)的主键实际上是个复合主键,同时使用了2个字段。这也暗示了对于同一个
person不能有重复的email地址,这正是Java里面使用Set时候所需要的语义(Set里元素不能重复)。
你现在可以试着把元素加入这个集合,就像我们在之前关联person和event的那样。Java里面的代码是相同的。
下面我们将映射一个双向关联(bi-directional association)- 在Java里面让person和event可以从关联的
任何一端访问另一端。当然,数据库表定义没有改变,我们仍然需要多对多(many-to-many)的阶数(multiplicity)。一个关系型数据库要比网络编程语言
更加灵活,所以它并不需要任何像导航方向(navigation direction)的东西 - 数据可以用任何可能的方式进行查看和获取。
首先,把一个参与者(person)的集合加入Event类中:
private Set participants = new HashSet();
public Set getParticipants() {
return participants;
}
public void setParticipants(Set participants) {
this.participants = participants;
}
在Event.hbm.xml里面也映射这个关联。
<set name="participants" table="PERSON_EVENT" inverse="true">
<key column="EVENT_ID"/>
<many-to-many column="PERSON_ID" class="Person"/>
</set>
如你所见,2个映射文件里都有通常的set映射。注意key和many-to-many
里面的字段名在两个映射文件中是交换的。这里最重要的不同是Event映射文件里set元素的
inverse="true"参数。
这个表示Hibernate需要在两个实体间查找关联信息的时候,应该使用关联的另外一端 - Person类。
这将会极大的帮助你理解双向关联是如何在我们的两个实体间创建的。
首先,请牢记在心,Hibernate并不影响通常的Java语义。
在单向关联中,我们是怎样在一个Person和一个Event之间创建联系的?
我们把一个Event的实例加到一个Person类内的Event集合里。所以,显然如果我们要让这个关联可以双向工作,
我们需要在另外一端做同样的事情 - 把Person加到一个Event类内的Person集合中。
这“在关联的两端设置联系”是绝对必要的而且你永远不应该忘记做它。
许多开发者通过创建管理关联的方法来保证正确的设置了关联的两端,比如在Person里:
protected Set getEvents() {
return events;
}
protected void setEvents(Set events) {
this.events = events;
}
public void addToEvent(Event event) {
this.getEvents().add(event);
event.getParticipants().add(this);
}
public void removeFromEvent(Event event) {
this.getEvents().remove(event);
event.getParticipants().remove(this);
}
注意现在对于集合的get和set方法的访问控制级别是protected - 这允许在位于同一个包(package)中的类以及继承自这个类的子类
可以访问这些方法,但是禁止其它的直接外部访问,避免了集合的内容出现混乱。你应该尽可能的在集合所对应的另外一端也这样做。
inverse映射参数究竟表示什么呢?对于你和对于Java来说,一个双向关联仅仅是在两端简单的设置引用。然而仅仅这样
Hibernate并没有足够的信息去正确的产生INSERT和UPDATE语句(以避免违反数据库约束),
所以Hibernate需要一些帮助来正确的处理双向关联。把关联的一端设置为inverse将告诉Hibernate忽略关联的
这一端,把这端看成是另外一端的一个镜子(mirror)。这就是Hibernate所需的信息,Hibernate用它来处理如何把把
一个数据导航模型映射到关系数据库表定义。
你仅仅需要记住下面这个直观的规则:所有的双向关联需要有一端被设置为inverse。在一个一对多(one-to-many)关联中
它必须是代表多(many)的那端。而在多对多(many-to-many)关联中,你可以任意选取一端,两端之间并没有差别。
二、引用Huihoo翻译Hibernate官方文档
22.1. 关于collections需要注意的一点
Hibernate collections被当作其所属实体而不是其包含实体的一个逻辑部分。这非常重要!它主要体现为以下几点:
当删除或增加collection中对象的时候,collection所属者的版本值会递增。
如果一个从collection中移除的对象是一个值类型(value type)的实例,比如composite
element,那么这个对象的持久化状态将会终止,其在数据库中对应的记录会被删除。同样的,向collection增加一个value
type的实例将会使之立即被持久化。
另一方面,如果从一对多或多对多关联的collection中移除一个实体,在缺省情况下这个对象并不会被删除。这个行为是完全合乎逻辑的--改变一个实
体的内部状态不应该使与它关联的实体消失掉!同样的,向collection增加一个实体不会使之被持久化。
实际上,向Collection增加一个实体的缺省动作只是在两个实体之间创建一个连接而已,同样移除的时候也只是删除连接。这种处理对于所有的情况都是合适的。对于父子关系则是完全不适合的,在这种关系下,子对象的生存绑定于父对象的生存周期。
22.2. 双向的一对多关系(Bidirectional one-to-many)
假设我们要实现一个简单的从Parent到Child的<one-to-many>关联。
<set name="children">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>
如果我们运行下面的代码
Parent p = .....;
Child c = new Child();
p.getChildren().add(c);
session.save(c);
session.flush();
Hibernate会产生两条SQL语句:
一条INSERT语句,为c创建一条记录
一条UPDATE语句,创建从p到c的连接
这样做不仅效率低,而且违反了列parent_id非空的限制。我们可以通过在集合类映射上指定not-null="true"来解决违反非空约束的问题:
<set name="children">
<key column="parent_id" not-null="true"/>
<one-to-many class="Child"/>
</set>
然而,这并非是推荐的解决方法。
这种现象的根本原因是从p到c的连接(外键parent_id)没有被当作Child对象状态的一部分,因而没有在INSERT语句中被创建。因此解决的办法就是把这个连接添加到Child的映射中。
<many-to-one name="parent" column="parent_id" not-null="true"/>
(我们还需要为类Child添加parent属性)
现在实体Child在管理连接的状态,为了使collection不更新连接,我们使用inverse属性。
<set name="children" inverse="true">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>
下面的代码是用来添加一个新的Child
Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c);
session.save(c);
session.flush();
现在,只会有一条INSERT语句被执行!
为了让事情变得井井有条,可以为Parent加一个addChild()方法。
public void addChild(Child c) {
c.setParent(this);
children.add(c);
}
现在,添加Child的代码就是这样
Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.save(c);
session.flush();
22.3. 级联生命周期(Cascading lifecycle)
需要显式调用save()仍然很麻烦,我们可以用级联来解决这个问题。
<set name="children" inverse="true" cascade="all">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>
这样上面的代码可以简化为:
Parent p = (Parent) session.load(Parent.class, pid);
Child c = new Child();
p.addChild(c);
session.flush();
同样的,保存或删除Parent对象的时候并不需要遍历其子对象。
下面的代码会删除对象p及其所有子对象对应的数据库记录。
Parent p = (Parent) session.load(Parent.class, pid);
session.delete(p);
session.flush();
然而,这段代码
Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();
不会从数据库删除c;它只会删除与p之间的连接(并且会导致违反NOT NULL约束,在这个例子中)。你需要显式调用delete()来删除Child。
Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
session.delete(c);
session.flush();
在我们的例子中,如果没有父对象,子对象就不应该存在,如果将子对象从collection中移除,实际上我们是想删除它。要实现这种要求,就必须使用cascade="all-delete-orphan"。
<set name="children" inverse="true" cascade="all-delete-orphan">
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>
注意:即使在collection一方的映射中指定inverse="true",级联仍然是通过遍历collection中的元素来处理的。如果你想要通过级联进行子对象的插入、删除、更新操作,就必须把它加到collection中,只调用setParent()是不够的。
22.4. 级联与未保存值(Cascades and unsaved-value)
假设我们从Session中装入了一个Parent对象,用户界面对其进行了修改,然后希望在一个新的Session里面调用update()来保存这些修改。对象Parent包含了子对象的集合,由于打开了级联更新,Hibernate需要知道哪些Child对象是新实例化的,哪些代表数据库中已经存在的记录。我们假设Parent和Child对象的标识属性都是自动生成的,类型为java.lang.Long。Hibernate会使用标识属性的值,和version 或 timestamp 属性,来判断哪些子对象是新的。(参见第 11.7 节 “自动状态检测”.) 在 Hibernate3 中,显式指定unsaved-value不再是必须的了。
下面的代码会更新parent和child对象,并且插入newChild对象。
//parent and child were both loaded in a previous session
parent.addChild(child);
Child newChild = new Child();
parent.addChild(newChild);
session.update(parent);
session.flush();
Well, that's all very well for the case of a generated identifier, but what about assigned identifiers
and composite identifiers? This is more difficult, since Hibernate can't use the identifier property to
distinguish between a newly instantiated object (with an identifier assigned by the user) and an
object loaded in a previous session. In this case, Hibernate will either use the timestamp or version
property, or will actually query the second-level cache or, worst case, the database, to see if the
row exists.
这对于自动生成标识的情况是非常好的,但是自分配的标识和复合标识怎么办呢?这是有点麻烦,因为Hibernate没有办法区分新实例化的对象(标识被用
户指定了)和前一个Session装入的对象。在这种情况下,Hibernate会使用timestamp或version属性,或者查询第二级缓存,或
者最坏的情况,查询数据库,来确认是否此行存在。
三、实战总结
1、inverse类似UML图中的导向性.one-many时,如果你设计的类是先整体,后部分,那么整体一端应该inverse为true。
many-many时,如果其中一端是业务语义的根,另一端的存在依附于根,那么根这一端的inverse为true,否则配置为false.如果两端都配置为true,关联表中就无法写入数据;如果两端都配置为false,你就不能同时a.addB(b),b.addA(a)的方式互相放入对方的集合中,否则会发生冲突。
2、Cascading配置了类之间的行为影响程度。本文引用的第二大节讲的很清楚。在实际使用时,还是要注意其用法与业务的关联:
如果只删除关联关系,通常配置save-update / none
如果把子端全部删除,配置为all。
3、Set在使用时。many-one,删除one时,cascading必须为all。如果是save-update,none都会出现“违反完整约束条件”的错误。
many-many(A-B)时,A方cascading为all时,删除A类,B、AB关联信息都会被删除。
A方cascading为save-update时,删除A类,AB关联信息会被删除,B保留。
A方cascading为none时,删除A类,B、AB关联信息都会保留。
4、List使用时,首先必须确保index的值必须设置,否则违背了List顺序位置的概念,无法完成删除逻辑的正确性,且会throw“违反完整约束条件”的错误。
在正确给定了index后,其行为与Set相同。