Hibernate入門記 コレクションその1 set で one-to-many
今回からはいよいよ「Chapter 6. Collection Mapping」へ突入です.
個人的には,セッションの後処理すらしないまま突き進んでいるのが無念なので(でも説明が出てくるまで意地でもこのままやっていこうかと),そのあたりを早く学習したい気もするのですが,ひとまずリファレンスの順番に従っていくことにします.
・・・
なのですが,どうもこの6章,リファレンスを見ながら書き進めるのが難しい.実は昨日,結構な量の文章を書いたのですが,いつまでたってもサンプルを書けるようにならないのです.それに,その書いた内容ってリファレンスの翻訳と変わらないじゃんみたいな.そんなのはとっくにozaccさんがやっておられるわけで... 無念だ.
ということで,いっそリファレンスの順番は無視して,自分なりの構成で書くことにしました.本来この入門記は入門する過程の記録ということで,ドキュメントを読んだ順番そのままに書くことを原則としていたのですが,今回はちょっと無理です〜.
ともあれ,始まり始まりぃ.
まず,今回学習する6章のタイトルは「コレクション」です.つまり,Hibernateは対多関連をコレクションで扱うということですね.そんなわけで,永続クラスは素直にコレクションをプロパティとして持てばいいということみたい.
それで,どんなコレクションが扱えるかというと,次のものらしいです.
- セット
- 要素の重複が許されないコレクションです.本来のセットは順序もないはずですが,そうでもないようです.
<set>
要素でマッピングの定義をします.
プロパティの型としては,java.util.Set
またはjava.util.SortedSet
を使うことができるようです. - マップ
- キーで要素を参照できるコレクションです.
<map>
要素でマッピングの定義をします.
プロパティの型としては,java.util.Map
またはjava.util.SortedMap
を使うことができるようです. - リスト
- 要素をインデックスで参照できるコレクションです.
<list>
要素,<array>
要素,<primitive-array>
要素でマッピングの定義をします.
プロパティの型としては,java.util.List
または 配列 を使うことができるようです. - バッグ(bag)
- 要素の重複ができるコレクションです.
<bag>
要素,<id-bag>
要素でマッピングの定義をします.
プロパティの型としては,java.util.Collection
またはjava.util.List
を使うことができるようです.
永続クラスは,上記のコレクション型のプロパティを持つことになるのですが,そこにはたくさんの注意事項があるようです.
- プロパティの型
- プロパティの型には
interface
を使え,とのことです.HashSet
やArrayList
ではなく,Set
やList
を使おうということですね. - コレクションのセマンティクス
- 永続化されるコレクションは,それぞれの
interface
が持つセマンティクスを拡張しないとのことです.
例として,コレクションの実装としてLinkedHashSet
を使ったとしても,挿入順でイテレートできるとは限らない(それはSet
では保証されていない)ということです. - コレクションの実装型と同一性
- Hibernateはコレクションのインスタンスを置き換えるとのことです.
その際,元々プロパティに設定されていたコレクションの実装クラスと同じ実装クラスになるとは限りません.
当然ながら,同一性も保証されないということになります. - 値型のルールに従う
- コレクションは値型と同様に,共有されず,コレクションを含むエンティティと共に永続化および削除されます.
null
をサポートしない- Hibernate は,
null
の参照と空のコレクションを区別しないそうです. - コレクションを渡すということ
- ある永続オブジェクトから別の永続オブジェクトにコレクションを渡すということは,(コピーではなく)移動になる...のか?
詳しくは双方向の関連のところで解説されるらしいですが,心配しなくてもいいらしいので,気にしない,気にしない.
ふむふむ.たくさんありますが,コレクションを渡すところ以外はあまり問題になりそうな感じじゃないですね.普通に使っていれば吐血しなくてもいいはず.たぶん.
次に,コレクションの要素として何が使えるかというと,次のものらしいです.
- エンティティ
- いわゆる永続クラスで,参照渡しのセマンティクスを持ちます.
<one-to-many>
要素(1対多の場合)または<many-to-many>
要素(多対多の場合)でマッピングを指定します. - 値型
- 前回学習したやつで,値渡しのセマンティクスを持ちます.
<element>
要素(単一カラムの場合)または<composite-element>
要素(複数カラムの場合)でマッピングを指定します.
要素にできないものとしては,コレクションがあげられています.また,リストなコレクションで配列を使う場合を除けばプリミティブ型もだめでしょう.
その他(?),ヘテロな関連ということで,<many-to-any>
要素,<index-many-to-any>
要素というのもあるらしいですが,あまり使わないみたいで説明がショボイです.Anyは継承絡みということでスキップ中なので,今回もスキップです.心より恥じる.
ということで,コレクションとその要素の組み合わせを一つずつ学習していけばよさげ.それならなんとか書いていけそう.よかったよかった.
まずは基本となるコレクションとしてセットに挑みたいと思います.セットにキーを付けるとマップ,インデックスを付ければリスト,重複を許せばバッグになるので,まずはセットかなと.その要素としてまずはエンティティ,多重度は1対多というのが基本中の基本に違いないと決定しました.
それで今回のタイトルとなったのです♪
では,早速
<set>
要素
を見ていきましょう.
リファレンスでは <map>
要素の属性が解説されているのですが,<set>
要素もおおむね同じみたい.ということで,ざくっと属性を概観します.
name
- 唯一の必須属性で,プロパティの名前です.
access
- プロパティにアクセスする方法を指定します.デフォルトは
property
です. table
- 多対多関連の場合の関連テーブルの名前です.省略するとプロパティ名が使われます.
schema
- テーブル名をスキーマ名で修飾する場合に指定します.
lazy
- 遅延初期化を使用する場合に
true
を指定します.デフォルトはfalse
です. sort
- sortedなコレクションの場合に
natural
(要素の型がComarable
を実装したクラスの場合) またはComparable
を実装したクラスを指定します.デフォルトはunsorted
です. inverse
- 双方向関連の場合に
true
を指定します.デフォルトはfalse
です. cascade
- 関連先のエンティティをカスケードに更新・削除する場合に指定します.デフォルトは
none
です. order-by
- コレクションをイテレートするときの順序に使われるカラムを指定します.
"asc"
または"desc"
を加えることもできます.なぜかわかりませんがJDK1.4の場合のみ指定できるとのことです.あと,sort
との関係は? うーみゅ. where
- SQLのwhere句を指定することができるようです.
batch-size
- 遅延フェッチする場合のサイズを指定するそうです.よくわかりません.デフォルトは1だそうです.
outer-join
- 外部結合を使用する場合に指定します.
DTD的には他にも persister
や check
があるようですが,ここでは無視しておきましょう.
遅延初期化とバッチサイズが相変わらず謎なのですが,「6.5. Lazy Initialization」で解説されているようなので,コレクションと要素の組み合わせを一通り学習した後に遅延したいと思います.
sort
と order-by
については「6.6. Sorted Collections」に書いてあるので軽く見ておきましょう.
どうやら,sort
を指定するとコレクションの実装クラスとして java.util.TreeSet
を使うようです.
一方 order-by
を指定するとコレクションの実装クラスとして LinkedHashSet
が使われるとのこと.それでJDK1.4(以降)の場合のみなんですね.この場合,ソートはDB側で行われるのが sort
属性との違い.
なので,この二つの属性は排他的(sort
属性が "unsorted"
の場合のみ order-by
を指定できる)と考えていいでしょう.
それから,order-by
属性の値で指定する文字列は,HQLではなくてSQLなので注意とのこと.うーみゅ.
<set>
要素の残りの属性は大丈夫かなぁ.たぶん.
次は <set>
要素に記述できる内容(子要素)を見ていきましょう.
まず,必須の子要素として
<key>
要素
があります.
Hibernateでは関連は外部キーで表すとのことで,そのマッピング情報を記述する要素です.
次の属性があります.
column
- 外部キーのカラム名を指定します.
リファレンス的には column
属性は必須となっているのですが,DTD的には任意となっています.そして,foregin-key
という属性も定義されているのですが,リファレンスでは解説されていません.なぜ? 歴史的な理由でしょうか? 同じように,<column>
要素で外部キーを指定できるようにも見えるのですが,これもリファレンスでは解説されていないようです.複数のカラムからなる外部キー(Hibernateでいうところのレガシーなテーブル)の場合に使えそうな気がするのですが,今は無視しておきますか.無念だ.
注意事項として,1対多関連の場合の外部キーは,NOT NULLでなければならないようです.らじゃ.
それから,コレクションの要素についてのマッピング情報を記述する子要素を一つ記述します.今回はもちろん
<one-to-many>
要素
を使います.
こいつは一つだけ属性を持っています.
class
- コレクションの要素となるエンティティの型を指定します.
楽勝っぽい.
さて,これで準備完了かな? エンティティに対する1対多関連に必要な情報はそろった気がします.
ということで,やってみましょう.この一年,できるだけのことをやってみましょう.
まずはテーブルを用意しましょう.
CREATE TABLE MODEL ( ID INTEGER IDENTITY, NAME VARCHAR ) CREATE TABLE CM ( ID INTEGER IDENTITY, NAME VARCHAR, MODEL INTEGER )
いつも思うのですが,最初にテーブルを考えるってHibernate的にはどうよ? これって頭がO/Rマッピングに切り替わっていない証拠? 心より恥じる.
次行きますよ! 次,次!!(エビちゃん風)
モデルの永続クラス.
package study; import java.io.Serializable; import java.util.HashSet; import java.util.Set; import net.sf.hibernate.CallbackException; import net.sf.hibernate.Lifecycle; import net.sf.hibernate.Session; public class Model implements Lifecycle { int id = -1; String name; Set cm; public Model() { } public Model(String name) { this.name = name; this.cm = new HashSet(); } public String toString() { return name + ", " + cm; } public void onLoad(Session s, Serializable id) { System.out.println("onLoad() : " + toString()); } public boolean onSave(Session s) throws CallbackException { System.out.println("onSave() : " + toString()); return false; } public boolean onUpdate(Session s) throws CallbackException { System.out.println("onUpdate() : " + toString()); return false; } public boolean onDelete(Session s) throws CallbackException { System.out.println("onDelete() : " + toString()); return false; } }
モデルのマッピングファイル.
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" > <hibernate-mapping auto-import="false" package="study"> <class name="Model"> <id name="id" access="field" unsaved-value="-1"> <generator class="identity"/> </id> <property name="name" access="field"/> <set name="cm" access="field" cascade="all"> <key column="model"/> <one-to-many class="Cm"/> </set> </class> </hibernate-mapping>
続いてCMの永続クラス.
package study; import java.io.Serializable; import net.sf.hibernate.CallbackException; import net.sf.hibernate.Lifecycle; import net.sf.hibernate.Session; public class Cm implements Comparable, Lifecycle { int id = -1; String name; public Cm() { } public Cm(String name) { this.name = name; } public int compareTo(Object o) { return name.compareTo(((Cm) o).name); } public String toString() { return name; } public void onLoad(Session s, Serializable id) { System.out.println("onLoad() : " + toString()); } public boolean onSave(Session s) throws CallbackException { System.out.println("onSave() : " + toString()); return false; } public boolean onUpdate(Session s) throws CallbackException { System.out.println("onUpdate() : " + toString()); return false; } public boolean onDelete(Session s) throws CallbackException { System.out.println("onDelete() : " + toString()); return false; } }
CMのマッピングファイル
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" > <hibernate-mapping auto-import="false" package="study"> <class name="Cm"> <id name="id" access="field" unsaved-value="-1"> <generator class="identity"/> </id> <property name="name" access="field"/> </class> </hibernate-mapping>
そして hibernate.cfg.xml
ファイル.
<?xml version="1.0"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd" > <hibernate-configuration> <session-factory> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:hsql://localhost:9001</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <property name="dialect">net.sf.hibernate.dialect.HSQLDialect</property> <property name="show_sql">true</property> <mapping resource="study/Model.hbm.xml"/> <mapping resource="study/Cm.hbm.xml"/> </session-factory> </hibernate-configuration>
最後に実行用クラス.
package study; import java.util.Iterator; import net.sf.hibernate.Session; import net.sf.hibernate.SessionFactory; import net.sf.hibernate.cfg.Configuration; public class Main { public static void main(String[] args) { try { Configuration config = new Configuration(); SessionFactory factory = config.configure().buildSessionFactory(); Session session = factory.openSession(); Model yuri = new Model("蛯原友里"); yuri.cm.add(new Cm("アビバ")); yuri.cm.add(new Cm("ゼスプリ・ゴールドキウイ")); session.save(yuri); Model sayo = new Model("相沢紗世"); sayo.cm.add(new Cm("am/pm")); sayo.cm.add(new Cm("マルイ")); session.save(sayo); Model yu = new Model("山田優"); yu.cm.add(new Cm("テスティモ")); yu.cm.add(new Cm("アリィー")); session.save(yu); session.flush(); session = factory.openSession(); Iterator it = session.find("from study.Model").iterator(); while (it.hasNext()) { System.out.println(it.next()); } } catch (Throwable e) { e.printStackTrace(); } } }
紗世ちゃんといえば日経4946という気もしたのですが,今でも流れてますか? 最近見ていないので外しました.
優はボーダフォンの方がいいかと思ったのですが,やはり最近見かけてないのでカネボウでまとめてみました.そうそう,優ちゃんといえば07/04の「ジャンクSPORTS」に登場みたいですね.F1代表?
そんなことはどうでもよくて,実行!!
onSave() : 蛯原友里, [ゼスプリ・ゴールドキウイ, アビバ] Hibernate: insert into Model (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : ゼスプリ・ゴールドキウイ Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : アビバ Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : 相沢紗世, [マルイ, am/pm] Hibernate: insert into Model (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : マルイ Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : am/pm Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : 山田優, [テスティモ, アリィー] Hibernate: insert into Model (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : テスティモ Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() onSave() : アリィー Hibernate: insert into Cm (name, id) values (?, null) Hibernate: CALL IDENTITY() Hibernate: update Cm set model=? where id=? Hibernate: select model0_.id as id, model0_.name as name from Model model0_ Hibernate: select cm0_.id as id__, cm0_.model as model__, cm0_.id as id0_, cm0_.name as name0_ from Cm cm0_ where cm0_.model=? onLoad() : ゼスプリ・ゴールドキウイ onLoad() : アビバ onLoad() : 蛯原友里, [ゼスプリ・ゴールドキウイ, アビバ] Hibernate: select cm0_.id as id__, cm0_.model as model__, cm0_.id as id0_, cm0_.name as name0_ from Cm cm0_ where cm0_.model=? onLoad() : マルイ onLoad() : am/pm onLoad() : 相沢紗世, [am/pm, マルイ] Hibernate: select cm0_.id as id__, cm0_.model as model__, cm0_.id as id0_, cm0_.name as name0_ from Cm cm0_ where cm0_.model=? onLoad() : テスティモ onLoad() : アリィー onLoad() : 山田優, [アリィー, テスティモ] 蛯原友里, [ゼスプリ・ゴールドキウイ, アビバ] 相沢紗世, [am/pm, マルイ] 山田優, [アリィー, テスティモ]
おぉ,いい感じじゃないですか!
でも,CM の INSERT の時点では MODEL カラムには値を設定しないのですね.ということは,UPDATE が6回呼び出されているってこと? この UPDATE だとそうに決まってるよなぁ.うーん.
この場合,親である MODEL
を INSERT しないと子である CM
の外部キーを設定できないのですが,Hibernateは子を INSERT してから親を INSERT するので,こうなってしまうということでしょうか?
気になる場合は cascade
属性を none
にして,自分で save()
するのがよいかも.そのためには Cm
クラスにも Moel
クラスへの参照を持たせて,双方向関連にすべきか.うーみゅ.
ともあれ,実行結果だけは意図したとおり.
すでにお気づきの方もいらっしゃると思いますが,Cm
クラスは Comparable
を implements
しています.かなり手抜きな実装で心より恥じるですが.
ということで,sort
属性を追加してみましょう.
<set name="cm" access="field" cascade="all" sort="natural">
そして実行!!
onSave() : 蛯原友里, [アビバ, ゼスプリ・ゴールドキウイ] java.lang.ClassCastException at net.sf.hibernate.type.SortedSetType.wrap(SortedSetType.java:31) at net.sf.hibernate.impl.WrapVisitor.processArrayOrNewCollection(WrapVisitor.java:78) at net.sf.hibernate.impl.WrapVisitor.processCollection(WrapVisitor.java:49) at net.sf.hibernate.impl.AbstractVisitor.processValue(AbstractVisitor.java:69) at net.sf.hibernate.impl.WrapVisitor.processValues(WrapVisitor.java:93) at net.sf.hibernate.impl.SessionImpl.doSave(SessionImpl.java:920) at net.sf.hibernate.impl.SessionImpl.doSave(SessionImpl.java:857) at net.sf.hibernate.impl.SessionImpl.saveWithGeneratedIdentifier(SessionImpl.java:775) at net.sf.hibernate.impl.SessionImpl.save(SessionImpl.java:738) at study.Main.main(Main.java:19)
ぐはぁっ.もはやお約束ですか.心より恥じる.
うーみゅ,保存のところでキャストに失敗してますね.ってことはなんですか,最初から SortedSet
になっていないとだめってことですか.
しょうがないので Model
のコンストラクタを次のように修正(import
も).
this.cm = new TreeSet();
そして実行!!
蛯原友里, [アビバ, ゼスプリ・ゴールドキウイ] 相沢紗世, [am/pm, マルイ] 山田優, [アリィー, テスティモ]
いいねぇ!(昔のアコム風)
次は当然,order-by
です.
モデルのマッピングファイルを次のように変更します.
<set name="cm" access="field" cascade="all" order-by="name desc">
そして実行!!
蛯原友里, [ゼスプリ・ゴールドキウイ, アビバ] 相沢紗世, [マルイ, am/pm] 山田優, [テスティモ, アリィー]
やったね!!
最後に,sort
属性と order-by
属性を同時に指定してみましょう.
モデルのマッピングファイルを次のように変更します.
<set name="cm" access="field" cascade="all" sort="natural" order-by="name desc">
そして実行!!
蛯原友里, [アビバ, ゼスプリ・ゴールドキウイ] 相沢紗世, [am/pm, マルイ] 山田優, [アリィー, テスティモ]
あら,例外が吹っ飛んでくるかと思いきや,ちゃんと動いちゃいました.結果は sort
が勝ったようですね.でも,SQLを見ると order by が付いています.半端だなぁ.
ま,よい子はこんな矛盾した指定をするなってことですね.
それから,関連づけられたオブジェクトがない場合にどうなるのかを軽く試してみました.
まず,永続化する場合にはコレクションのプロパティ(この場合は cm
)が null
でも空の Set
でも同じように正しく動きました.
また,DBからロードした場合は,空の Set
がプロパティに設定されていました.ヌルポにならなくて安心っぽい.
ま,あまり気にしないでも大丈夫ってことみたいです.
次回は同じく <set>
で,<many-to-many>
に挑戦します.今度は双方向にしたいな♪
でもでも,明日はOOEnkaiなので,次回は明後日以降かも.心より恥じる.