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 を使え,とのことです.
HashSetArrayList ではなく,SetList を使おうということですね.
コレクションのセマンティクス
永続化されるコレクションは,それぞれの 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的には他にも persistercheck があるようですが,ここでは無視しておきましょう.
遅延初期化とバッチサイズが相変わらず謎なのですが,「6.5. Lazy Initialization」で解説されているようなので,コレクションと要素の組み合わせを一通り学習した後に遅延したいと思います.
sortorder-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 クラスは Comparableimplements しています.かなり手抜きな実装で心より恥じるですが.
ということで,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なので,次回は明後日以降かも.心より恥じる.