Hibernate.org Community Documentation

- 上一页
- 下一页

## 第 24 章 示例：父子关系（Parent/Child）

刚刚接触 Hibernate 的人大多是从父子关系（parent / child type relationship）的建模入手的。父子关系的建模有两种方法。由于种种原因，最方便的方法是把 `Parent` 和 `Child` 都建模成实体类，并创建一个从 `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 关系。

## 24.1. 关于 collections 需要注意的一点

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

- 当删除或增加 collection 中对象的时候，collection 所属者的版本值会递增。

- 如果一个从 collection 中移除的对象是一个值类型（value type）的实例，比如 composite element，那么这个对象的持久化状态将会终止，其在数据库中对应的记录会被删除。同样的，向 collection 增加一个 value type 的实例将会使之立即被持久化。

- 另一方面，如果从一对多或多对多关联的 collection 中移除一个实体，在缺省情况下这个对象并不会被删除。这个行为是完全合乎逻辑的－－改变一个实体的内部状态不应该使与它关联的实体消失掉。同样的，向 collection 增加一个实体不会使之被持久化。

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

## 24.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` 列 `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();

## 24.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()` 是不够的。

## 24.4. 级联与未保存值（`unsaved-value`）

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.

下面的代码会更新 `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();

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

## 24.5. 结论

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

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

版权 © 2004 Red Hat, Inc.

- 上一页第 23 章 Additional modules
- 上一级
- 起始页
- 下一页第 25 章 示例：Weblog 应用程序