Hibernate.orgCommunity Documentation

第22章 例: 親/子供

22.1. コレクションに関する注意
22.2. 双方向一対多
22.3. ライフサイクルのカスケード
22.4. カスケードと unsaved-value
22.5. 結論

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

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

その代わりに、デフォルトの動作では、エンティティをコレクションに追加すると単に二つのエンティティ間のリンクを作成し、一方エンティティを削除するとリンクも削除します。これはすべてのケースにおいて非常に適切です。これが適切でないのは親/子関係の場合です。この場合子供の生存は親のライフサイクルに制限されるからです。

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_id カラムにおいて NOT NULL 制約に違反します。コレクションのマッピングで not-null="true" と指定することで、 null 制約違反を解決することができます:


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

明示的に 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 制約違反を引き起こす)だけです。 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();

今このケースでは実際に 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() を呼ぶだけでは不十分です。

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 「自動的な状態検出」.) 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 はタイムスタンプかバージョンのプロパティのどちらかを使うか、二次キャッシュに問い合わせます。最悪の場合、行が存在するかどうかデータベースを見ます。

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

最初の段落で代替方法について触れました。上記のような問題は <composite-element> マッピングの場合は存在せず、にもかかわらずそれは確かに親子関係のセマンティクスを持ちます。しかし残念ながら、複合要素クラスには2つの大きな制限があります: 1つは複合要素はコレクションを持つことができないことです。もうひとつは、ユニークな親ではないエンティティの子供となるべきではないということです