Hibernate.orgCommunity Documentation

第 24 章 示例:父子关系(Parent/Child)

24.1. 关于 collections 需要注意的一点
24.2. 双向的一对多关系(Bidirectional one-to-many)
24.3. 级联生命周期(Cascading lifecycle)
24.4. 级联与未保存值(unsaved-value)
24.5. 结论

刚刚接触 Hibernate 的人大多是从父子关系(parent / child type relationship)的建模入手的。父子关系的建模有两种方法。由于种种原因,最方便的方法是把 ParentChild 都建模成实体类,并创建一个从 Parent 指向 Child 的 <one-to-many> 关联,对新手来说尤其如此。还有一种方法,就是将 Child 声明为一个 <composite-element>(组合元素)。 事实上在 Hibernate 中 one to many 关联的默认语义远没有 composite element 贴近 parent / child 关系的通常语义。下面我们会阐述如何使用带有级联的双向一对多关联(idirectional one to many association with cascades)去建立有效、优美的 parent / child 关系。

Hibernate collections 被当作其所属实体而不是其包含实体的一个逻辑部分。这非常重要,它主要体现为以下几点:

实际上,向 Collection 增加一个实体的缺省动作只是在两个实体之间创建一个连接而已,同样移除的时候也只是删除连接。这种处理对于所有的情况都是合适的。对于父子关系则是完全不适合的,在这种关系下,子对象的生存绑定于父对象的生存周期。

假设我们要实现一个简单的从 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 语句:

这样做不仅效率低,而且违反了 parent_idparent_id 非空的限制。我们可以通过在集合类映射上指定 not-null="true" 来解决违反非空约束的问题:


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

然而,这并非是推荐的解决方法。

这种现象的根本原因是从 pc 的连接(外键 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();

需要显式调用 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() 是不够的。

Suppose we loaded up a Parent in one Session, made some changes in a UI action and wanted to persist these changes in a new session by calling update(). The Parent will contain a collection of children and, since the cascading update is enabled, Hibernate needs to know which children are newly instantiated and which represent existing rows in the database. We will also assume that both Parent and Child have generated identifier properties of type Long. Hibernate will use the identifier and version/timestamp property value to determine which of the children are new. (See 第 11.7 节 “自动状态检测”.) In Hibernate3, it is no longer necessary to specify an unsaved-value explicitly.

下面的代码会更新 parentchild 对象,并且插入 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();

这对于自动生成标识的情况是非常好的,但是自分配的标识和复合标识怎么办呢?这是有点麻烦,因为 Hibernate 没有办法区分新实例化的对象(标识被用户指定了)和前一个 Session 装入的对象。在这种情况下,Hibernate 会使用 timestamp 或 version 属性,或者查询第二级缓存,或者最坏的情况,查询数据库,来确认是否此行存在。

这里有不少东西需要融会贯通,可能会让新手感到迷惑。但是在实践中它们都工作地非常好。大部分 Hibernate 应用程序都会经常用到父子对象模式。

在第一段中我们曾经提到另一个方案。上面的这些问题都不会出现在 <composite-element> 映射中,它准确地表达了父子关系的语义。很不幸复合元素还有两个重大限制:复合元素不能拥有 collections,并且,除了用于惟一的父对象外,它们不能再作为其它任何实体的子对象。