第16章 例: 親/子関係

新規ユーザがHibernateを使いまず最初に扱うモデルの一つは、親子型の関係です。これには異なった2つのアプローチが存在します。とりわけ新規ユーザにとって、さまざまな理由から最も便利だと思われるアプローチは、からへの<one-to-many> 関連によりの両方をエンティティクラスとしてモデリングする方法です。(もう一つの方法は、<composite-element>として定義するものです。)これまでで、(Hibernateでは)one-to-many関連のデフォルトの意味が、通常複合要素のマッピングよりも親子関係の意味にかなり近いことがわかりました。 それでは親子関係を効率的かつエレガントにモデリングするために、どのようにしてカスケード操作を使った双方向one-to-many関連を扱うのか説明します。それはまったく難しいものではありません!

16.1. コレクションについての注意

Hibernateのコレクションは所持するエンティティの論理的な部分と考えられ、それは所持されるエンティティのでは決してありません。これは致命的な違いです!以下のような結果になります:

  • オブジェクトをコレクションから削除、またはコレクションに追加するとき、コレクションのオーナのバージョン番号はインクリメントされます。

  • もしコレクションから削除されるオブジェクトがバリュー・タイプのインスタンス(例えばコンポジット・エレメント)だったならば、そのオブジェクトは永続化されず、その状態はデータベースから完全に削除されます。同じように、バリュー・タイプのインスタンスをコレクションに追加すると、その状態は即座に永続化されます。

  • その一方、もしエンティティがコレクション(one-to-manyまたはmany-to-many関連)から削除されても、デフォルトではそれは削除されません。この振る舞いは完全に首尾一貫しています。すなわち、他のエンティティの内部状態への変更によって、関連するエンティティが消滅すべきではないということです!同様に、エンティティがコレクションに追加されても、デフォルトではそのエンティティは永続化されません。

その代わりに、デフォルトの振る舞いでは、エンティティをコレクションに追加すると単に2つのエンティティ間のリンクを作成し、一方でエンティティを削除するとリンクを削除します。これはすべてのケースにおいて、非常に適切だと言えます。しかし子が親のライフサイクルに制限される親子関係のケースでは、全く適切ではありません。

16.2. 双方向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は、2つのSQL文を発行します:

  • cに対するレコードを生成するINSERT

  • pからcへのリンクを作成するUPDATE

これは非効率的なだけではなく、parent_idカラムにおいてNOT NULL制約に違反します。

pからcへのリンク(外部キーparent_id)はChildオブジェクトの状態の一部とは考えられず、そのためINSERTによって外部キーは作成されないことが原因です。ですから、解決策はChildマッピングの一部にリンクすることです。

<many-to-one name="parent" column="parent_id" not-null="true"/>

(またChildクラスにparentプロパティを追加する必要があります。

さあそれではChildエンティティがリンクの状態をマッピングするようになったので、コレクションがリンクを更新しないようにしましょう。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();

そして今、たった一つのSQL INSERTだけが発行されるようになりました!

厳しくするには、ParentaddChild()メソッドを作成します。

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();

16.3. ライフサイクルのカスケード

明示的に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();

しかし、次のコードはデータベースからcを削除しません。pへのリンクを削除する(そしてこのケースではNOT NULL制約違反を引き起こす)だけです。

Parent p = (Parent) session.load(Parent.class, pid);
Child c = (Child) p.getChildren().iterator().next();
p.getChildren().remove(c);
c.setParent(null);
session.flush();

以下のように明示的に子をdelete()する必要があります。 delete() the 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();

さあ私たちのケースでは実際にChildが親なしでは存在できないようになりました。そのため、もしコレクションからChildを取り除けば、本当に削除したいものが取り除かれます。 このためにcascade="all-delete-orphan"を使わなければなりません。

<set name="children" inverse="true" cascade="all-delete-orphan">
    <key column="parent_id"/>
    <one-to-many class="Child"/>
</set>

注意:コレクションのマッピングでinverse="true"と指定しても、コレクションの要素の繰り返しによってカスケードが依然実行されます。それゆえもしカスケードでオブジェクトをセーブ、削除、更新する必要があるなら、それをコレクションに追加しなければなりません。単にsetParent()をコールするだけでは不十分です。

16.4. カスケードupdate()の使用

ParentがあるSessionでロードされ、UIのアクションで変更が加えられ、この変更を新しいSessionにおいて永続化(update()をコールして)したいとします。Parentがのコレクションを持ち、カスケード更新が有効になっているため、 Hibernateはどの子が新しくインスタンス化されたか、どれがデータベースに行として表現されているかということを 知る必要があります。 ParentChildの両方がjava.lang.Long型の(人工的)識別プロパティを持つと仮定しましょう。Hibernateはどの子が新しいのかを決定するために識別プロパティの値を使います(versionやtimestampプロパティも使えます。項9.4.2. 「関連付けをやめたオブジェクトの更新」参照)。

unsaved-value属性は新しくインスタンス化されたインスタンスの識別子の値を特定するために使われます。unsaved-value のデフォルト値はnullで、これは Long識別子型にとって十分です。もしプリミティブ識別プロパティを使うならば、Childマッピングに以下を指定する必要があります。:

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

(versionとtimestampプロパティ・マッピングのunsaved-value属性も存在します。)

以下のコードはparentchildを更新し、newChild.を挿入します。

//parentとchildは両方とも前のSessionでロードされていますn
parent.addChild(child);
Child newChild = new Child();
parent.addChild(newChild);
session.update(parent);
session.flush();

これらは生成された識別子の場合には非常に良いのですが、割り当てられた識別子と複合識別子の場合はどうでしょうか?これはunsaved-valueが、(ユーザにより割り当てられた識別子を持つ)新しくインスタンス化されたオブジェクトと前のSessionでロードされたオブジェクトを区別できないためより難しいです。この場合多分Hibernateにヒントを与える必要があります。それらは

  • <version>においてunsaved-value="null"またはunsaved-value="negative"、またはクラスに対して<timestamp>プロパティ・マッピングを定義します。

  • unsaved-value="none"を設定し、update(parent)をコールする前に新しくインスタンス化された子を明示的にsave()します。

  • unsaved-value="any"を設定し、update(parent)をコールする前に以前の永続の子を明示的にupdate()します。

noneは割り当てられた識別子と複合識別子のデフォルトのunsaved-valueです。

一つの更なる可能性があります。isUnsaved()という名前の新しいInterceptorメソッドがあります。このメソッドは新しくインスタンス化されたオブジェクトを区別するための独自の戦略をアプリケーションに実装させることを可能にします。例えば、永続クラスに対して基本クラスを定義することもできます。

public class Persistent {
    private boolean _saved = false;
    public void onSave() {
        _saved=true;
    }
    public void onLoad() {
        _saved=true;
    }
    ......
    public boolean isSaved() {
        return _saved;
    }
}

(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;
}

16.5. 結論

ここではかなりの量を要約したので、最初の頃は混乱しているように思われるかもしれません。しかし実際は、すべて非常に良く動作します。ほとんどのHibernateアプリケーションでは多くの局面でで親子パターンを使用しています。

最初の段落で代わりの方法につい触れました。それらは<composite-element>マッピングの場合は 存在せず、それは確かに親子関係の意味を持ちます。不幸にも複合要素クラスには二つの大きな制限があります。:一つは複合要素がコレクションを持つことができないこと。もう一つは他のユニークな親ではないエンティティの子となるべきではないということです。(しかし、それらは<idbag>マッピングを使って、代理の主キーを持てます。)