例:親/子供
新規ユーザがHibernateを使ってまず最初に扱うモデルの一つに、親子型のモデル化があります。
このモデル化には二つのアプローチが存在します。とりわけ新規ユーザにとって、
さまざまな理由から最も便利だと思われるアプローチは、親 から 子供
への <one-to-many> 関連により 親 と 子供
の両方をエンティティクラスとしてモデリングする方法です
(もう一つの方法は、子供 を <composite-element> として定義するものです)。
これで(Hibernateにおける)一対多関連のデフォルトのセマンティクスが、通常の複合要素のマッピングよりも、
親子関係のセマンティクスから遠いことがわかります。
それでは親子関係を効率的かつエレガントにモデリングするために、
カスケード操作を使った双方向一対多関連 の扱い方を説明します。これはまったく難しいものではありません。
コレクションに関する注意
Hibernateのコレクションは自身のエンティティの論理的な部分と考えられ、
決して包含するエンティティのものではありません。これは致命的な違いです!
これは以下のような結果になります:
オブジェクトをコレクションから削除、またはコレクションに追加するとき、
コレクションのオーナーのバージョン番号はインクリメントされます。
もしコレクションから削除されたオブジェクトが値型のインスタンス
(例えばコンポジットエレメント)だったならば、そのオブジェクトは永続的ではなくなり、
その状態はデータベースから完全に削除されます。
同じように、値型のインスタンスをコレクションに追加すると、その状態はすぐに永続的になります。
一方、もしエンティティがコレクション(一対多または多対多関連)から削除されても、
デフォルトではそれは削除されません。この動作は完全に一貫しています。
すなわち、他のエンティティの内部状態を変更しても、関連するエンティティが消滅すべきではないということです。
同様に、エンティティがコレクションに追加されても、デフォルトではそのエンティティは永続的にはなりません。
その代わりに、デフォルトの動作では、エンティティをコレクションに追加すると単に二つのエンティティ間のリンクを作成し、
一方エンティティを削除するとリンクも削除します。これはすべてのケースにおいて非常に適切です。
これが適切でないのは親/子関係の場合です。この場合子供の生存は親のライフサイクルに制限されるからです。
双方向一対多
Parent から Child への単純な <one-to-many> 関連から始めるとします。
]]>
以下のコードを実行すると、
Hibernateは二つのSQL文を発行します:
cに対するレコードを生成するINSERT
pからcへのリンクを作成するUPDATE
これは非効率的なだけではなく、parent_id カラムにおいて NOT NULL 制約に違反します。
コレクションのマッピングで not-null="true" と指定することで、null制約違反を解決することができます:
]]>
しかしこの解決策は推奨できません。
この動作の根本的な原因は、p から c へのリンク
(外部キー parent_id)は Child オブジェクトの状態の一部とは考えられず、
そのため INSERT によってリンクが生成されないことです。
ですから、解決策はリンクをChildマッピングの一部にすることです。
]]>
(また Child クラスに parent プロパティを追加する必要があります。)
それでは Child エンティティがリンクの状態を制御するようになったので、
コレクションがリンクを更新しないようにしましょう。それには inverse 属性を使います。
]]>
以下のコードを使えば、新しい Child を追加することができます。
これにより、SQLの INSERT 文が一つだけが発行されるようになりました!
もう少し強化するには、Parent の addChild() メソッドを作成します。
Child を追加するコードはこのようになります。
ライフサイクルのカスケード
明示的に save() をコールするのはまだ煩わしいものです。これをカスケードを使って対処します。
]]>
これにより先ほどのコードをこのように単純化します
同様に Parent を保存または削除するときに、子供を一つ一つ取り出して扱う必要はありません。
以下のコードは p を削除し、そしてデータベースからその子供をすべて削除します。
しかしこのコードは
データベースから c を削除しません。p へのリンクを削除する
(そしてこのケースでは NOT NULL 制約違反を引き起こす)だけです。
Child の delete() を明示する必要があります。
今このケースでは実際に Child が親なしでは存在できないようになりました。
そのため、もしコレクションから Child を取り除く場合、これも削除したいです。
そのためには cascade="all-delete-orphan" を使わなければなりません。
]]>
注意:コレクションのマッピングで inverse="true" と指定しても、
コレクションの要素のイテレーションによって、依然カスケードが実行されます。
そのためもしカスケードでオブジェクトをセーブ、削除、更新する必要があるなら、
それをコレクションに追加しなければなりません。単に setParent() を呼ぶだけでは不十分です。
カスケードと unsaved-value
Parent が、ある Session でロードされ、UIのアクションで変更が加えられ、
update() を呼んでこの変更を新しいセッションで永続化したいとします。
Parent が子供のコレクションを持ち、カスケード更新が有効になっているため、
Hibernateはどの子供が新しくインスタンス化されたか、どれがデータベースの既存の行に相当するのかを知る必要があります。
Parent と Child の両方が java.lang.Long
型の識別プロパティを生成したとしましょう。
Hibernateはどの子供が新しいものかを決定するために識別プロパティの値を使います(versionやtimestampプロパティも使えます。
参照)。Hibernate3になって、
明示的に unsaved-value を指定する必要はなくなりました。
以下のコードは parent と child を更新し、newChild を挿入します。
これらは生成された識別子の場合には非常に良いのですが、割り当てられた識別子と複合識別子の場合はどうでしょうか?
これはHibernateが、(ユーザにより割り当てられた識別子を持つ)新しくインスタンス化されたオブジェクトと、
以前のSessionでロードされたオブジェクトを区別できないため、より難しいです。
この場合、Hibernateはタイムスタンプかバージョンのプロパティのどちらかを使うか、二次キャッシュに問い合わせます。
最悪の場合、行が存在するかどうかデータベースを見ます。
結論
ここではかなりの量を要約したので、最初の頃は混乱しているように思われるかもしれません。
しかし実際は、すべて非常に良く動作します。ほとんどのHibernateアプリケーションでは、多くの場面で親子パターンを使用します。
最初の段落で代替方法について触れました。上記のような問題は <composite-element> マッピングの場合は存在せず、
にもかかわらずそれは確かに親子関係のセマンティクスを持ちます。
しかし残念ながら、複合要素クラスには二つの大きな制限があります:
1つは複合要素はコレクションを持つことができないことです。もうひとつは、
ユニークな親ではないエンティティの子供となるべきではないということです