Chapter 9. 父子关系(Parent Child Relationships)

刚刚接触Hibernate的人大多是从父子关系(parent / child type relationship)的建模入手的。父子关系的建模有两种方法。比较简便、直观的方法就是在实体类ParentChild之间建立<one-to-many>的关联关系,从Parent指向Child,对新手来说尤其如此。但还有另一种方法,就是将Child声明为一个<composite-element>(组合元素)。 可以看出在Hibernate中使用一对多关联比composite element更接近于通常parent / child关系的语义。下面我们会阐述如何使用双向可级联的一对多关联(bidirectional one to many association with cascades)去建立有效、优美的parent / child关系。这一点也不难!

9.1. 关于collections

在Hibernate下,实体类将collection作为自己的一个逻辑单元,而不是被容纳的多个实体。这非常重要!它主要体现为以下几点:

  • 当删除或增加collection中对象的时候,拥有这个collection的实体对象的版本值会递增。

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

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

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

9.2. 双向的一对多关系(Bidirectional one to many)

让我们从一个简单的例子开始,假设要实现一个从类Parent到类Child的一对多关系。

<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非空的限制。

底层的原因是,对象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();

9.3. 级联(Cascades)

对每个对象调用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();

will not remove c from the database; it will ony remove the link to p (and cause a NOT NULL constraint violation, in this case). You need to explicitly delete() the Child since, by design, Hibernate does not have a garbage collector! Use

不会从数据库删除c;它只会删除与p之间的连接(并且会导致违反NOT NULL约束,在这个例子中)。你需要明确调用Childdelete()方法。

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()是不够的。

9.4. 级联更新(Using cascading update()

假设我们从Session中装入了一个Parent对象,用户界面对其进行了修改,然后我们希望在一个新的Session里面调用update()来更新它。对象Parent包含了子对象的集合,由于打开了级联更新,Hibernate需要知道哪些子对象是新的,哪些是数据库中已经存在的。我们假设ParentChild对象的标识属性的类型为java.lang.Long。Hibernate会使用标识属性的值来判断哪些子对象是新的。

unsaved-value属性是用来表示新实例的标识属性值的,缺省为"null",用在Long类型的标识类型再好不过了。如果我们使用原始类型作为标识类型的话,我们在配置Child类映射的时候就必须写:

<id name="id" type="long" unsaved-value="0">

(为版本和时间戳属性进行映射,也会有另一个叫做unsaved-value的属性。)

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

好的,对于自动生成标识的情况这样做很方便,但是自分配的标识和复合标识怎么办呢?这是有点麻烦,因为unsaved-value无法区分新对象(标识是用户指定的)和前一个Session装入的对象。在这种情况下,你可能需要给Hibernate一些提示,在调用update(parent)之前:

  • 在这个类的<version> or <timestamp>属性映射上定义unsaved-value="null"或者unsaved-value="negative"

  • 在对父对象执行update(parent)之前,设定unsaved-value="none"并且显式的调用save()在数据库创建新子对象

  • 在对父对象执行update(parent)之前,设定unsaved-value="any"并且显式的调用update()更新已经装入的子对象

none是自分配标识和复合标识的unsaved-value的缺省值。

There is one further possibility. There is a new Interceptor method named isUnsaved() which lets the application implement its own strategy for distinguishing newly instantiated objects. For example, you could define a base class for your persistent classes.

还有一种可能情况,有一个名为isUnsaved()拦截器(Interceptor)方法,它允许应用程序自己实现新实例的判断。比如,你可以自己定义一个持久类的祖先类:

public class Persistent implements Lifecycle {
    private boolean _saved = false;
    public boolean onSave(Session s) {
        _saved=true;
        return NO_VETO;
    }
    public void onLoad(Session s, Serializable id) {
        _saved=true;
    }
    ......
    public boolean isSaved() {
        return _saved;
    }
}

And implement isUnsaved()

saved属性是不会被持久化的。) 现在在onLoad()onSave()外,还要实现isUnsaved()

public Boolean isUnsaved(Object entity) {
    if (entity instanceof Persistent) {
        return new Boolean( !( (Persistent) entity ).isSaved() );
    }
    else {
        return null;
    }
}

public boolean onLoad(Object entity, 
    Serializable id,
    Object[] state,
    String[] propertyNames,
    Type[] types) {

    if (entity instanceof Persistent) ( (Persistent) entity ).onLoad();
    return false;
}

public boolean onSave(Object entity,
    Serializable id,
    Object[] state,
    String[] propertyNames,
    Type[] types) {
        
    if (entity instanceof Persistent) ( (Persistent) entity ).onSave();
    return false;
}

9.5. 结论

这个问题往往让新手感到迷惑,它确实不太容易消化。不过,经过一些实践以后,你会感觉越来越顺手。父子对象模式已经被广泛的应用在Hibernate应用程序中。

在第一段中我们曾经提到另一个方案。复合元素的语义与父子关系是等同的,但是我们并没有详细讨论。很不幸复合元素还有两个重大限制:复合元素不能拥有collections,并且,除了用于惟一的父对象外,它们不能再作为其它任何实体的子对象。(但是,通过使用<idbag>映射,它们可能拥有代理主键。)