Hibernate 入門記 コンポーネント

CanCam を買ってきたので Hibernate どころではなかったりもするのですが,とりあえず...
でも EJB3 はお休みかな.心より恥じる.


今日は「7. Component Mapping」です.
ここでの「コンポーネント」は JavaBeans や EJBコンポーネントとは全然関係なくて,コンポジション(複合)されるという程度の意味です.
コンポーネントは値型で,エンティティ (永続クラス) と異なり ID を持たず,共有されないのが特徴です.
さてそのコンポーネントですが,様々な状況で使えるように,多くの要素が用意されています.

  • <component> 要素
  • <dynamic-component> 要素
  • <composite-id> 要素
  • <composite-element> 要素
  • <composite-index> 要素

なーんだ,みんな学習済みじゃないですか.(^^;
このうち <composite-index> 要素だけは実際にお試ししていないのですが,その他は全部お試し済みです.やったね.
ということで,終〜了.


ってことはなくて,まだ学んでいないことが少しはあるみたい.
まずは <component> 要素について.
親 (集約元) となるエンティティへの参照を持つプロパティを持つことが出来ます.そのために使うのが,

  • <parent> 要素

です.こいつは一つだけ属性を持っています.

name
親となるエンティティへの参照を保持するプロパティ名を指定します.必須です.

あら? access 属性がないのですね.ということは getter/setter でアクセス出来なきゃいけないという事でしょうか.まぁいっか.
それから,<component>入れ子 (ネスト) は出来ないとのことです.


次に コレクションの要素となるコンポーネント<composite-element> 要素について.
<composite-element> 要素はコンポーネント (<component> 要素) を含むことが出来ます (ただし,コンポーネントのコレクションを含むことは出来ません).そのために使うのが

  • <nested-composite-element> 要素

です.そんなのまであったのか... ただし,<one-to-many> などにできないか考え直せとのことです.
ともかくその <nested-composite-element> 要素ですが,次の属性を持っています.

class
コンポーネントのクラス名を指定します.必須です.
name
プロパティ名を指定します.必須です.
access
Hibernate がこのプロパティにアクセスする方法を指定します.デフォルトは property でしょう.きっと.

そして内容としては,<parent> 要素 (任意) の後に <property> 要素・<many-to-one> 要素・<any> 要素・<nested-component-element> 要素を記述します.ということは,こいつは入れ子に出来るんですね.どんなときに入れ子にしたくなるのか想像が出来ませんけれど.
それから注意事項が.セットで <composite-element> を使う場合には,そのプロパティは全て NOT NULL でなければならないそうです.これは,Hibernate がコレクションの要素が削除されたことを確認するためにプロパティの値を使っているかららしいです.コンポーネントは ID プロパティを持っていないので,全てのプロパティが一致するかチェックするのだとか.もし NULL になる可能性のあるプロパティがあるのなら,セットではなくリストやマップ,バッグを使えとのことです.


続いてはエンティティの主キー (複合キー) としてコンポーネントマッピングするための <composite-id> 要素.
こいつでマッピングされるクラスは,Serializableimplements しなければならないとのことです.
それから,Session#saveOrUpdate() やカスケード更新などに対応するには,Intercepter という interfaceimplements して isUnsaved() を実装しろとのこと.
あるエンティティの主キーが複合キーということは,そのエンティティへの外部キーも複合キーになります.そこで,<key> 要素などでも複合キーを指定しなければなりません.これまで使ってきたように単一のカラムを持った外部キーの場合は <key> 要素の column 属性でそのカラムを指定していましたが,複合キーの場合は <key> 要素の内容として <column> 要素を (複数) 記述します.これは,<meny-to-one> 要素や <many-to-many> 要素でも同じです.


最後に,コンポーネントとしてマッピングされるクラスは,equals(Object)hashCode() をオーバーライドしなくてはなりません.リファレンス的には <component> なんかにはオーバーライドしろとは書いてないのですが,値型は無条件にオーバーライドすべきだと思います.


さてさて,それではお試しコーナーいってみましょう.
本来なら以前お試しをサボった <composite-index> 要素をお試しすべきのような気もするわけですが,あまり興味を持てません.
それよりも,<nested-composite-element> 要素の方がおもしろいかも? しかし使い道が... そもそも永続オブジェクトで値型との関連っていうのがなー.
まぁ,どうせ現実的なネタでやっているわけではないので,例によってモデルと雑誌でやってみますか.
いつもはモデルの方から雑誌への関連をたどっていくのが多かったのですが,今回は逆にして雑誌がモデルのセットを持つことにします.そのモデルはなぜか値型 (<composite-element> 要素) で,その名前がコンポーネント (<nested-composite-element> 要素) ということにしましょう.
ということでまずはテーブル定義.

CREATE TABLE MAGAZINE (
    ID INTEGER IDENTITY PRIMARY KEY,
    NAME VARCHAR
)

CREATE TABLE MODEL (
    FIRST_NAME VARCHAR,
    LAST_NAME VARCHAR,
    MAGAZINE INTEGER
)

モデルは値型にマッピングするので ID なしにしてみました.そんなんでいいのか?
ともあれ,次に名前のクラス(値型).

package study;

public class Name {
    String firstName;
    String lastName;
    
    public Name() {
    }
    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Name)) {
            return false;
        }
        Name other = (Name) o;
        return firstName.equals(other.firstName) && lastName.equals(other.lastName);
    }
    public int hashCode() {
        return toString().hashCode();
    }
    public String toString() {
        return firstName + " " + lastName;
    }
}

そしてこれを使うモデルのクラス(値型).

package study;

public class Model {
    Name name;

    public Model() {
    }
    public Model(Name name) {
        this.name = name;
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Model)) {
            return false;
        }
        Model other = (Model) o;
        return name.equals(other.name);
    }
    public int hashCode() {
        return name.hashCode();
    }
    public String toString() {
        return name.toString();
    }
}

プロパティ一つなんで,存在意義は全くないのですが... 心より恥じる.
そして雑誌の永続クラス (エンティティ).

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 Magazine implements Lifecycle {
    int id = -1;
    String name;
    Set model;

    public Magazine() {
    }
    public Magazine(String name) {
        this.name = name;
        this.model = new HashSet();
    }

    public String toString() {
        return name + " : " + model;
    }

    public void onLoad(Session s, Serializable id) {
        System.out.println("onLoad() : " + name);
    }
    public boolean onSave(Session s) throws CallbackException {
        System.out.println("onSave() : " + name);
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        System.out.println("onUpdate() : " + name);
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        System.out.println("onDelete() : " + name);
        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="Magazine">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="name" access="field"/>
        <set name="model" access="field" table="Model">
            <key column="magazine"/>
            <composite-element class="Model">
                <nested-composite-element name="name" class="Name" access="field">
                    <property name="firstName" column="first_name" access="field"/>
                    <property name="lastName" column="last_name" access="field"/>
                </nested-composite-element>
            </composite-element>
        </set>
    </class>
</hibernate-mapping>

このマッピングファイルを hibernate.cfg.xml に記述します.
最後に実行用のクラス.

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(new Name("Yuri", "Ebihara"));
            Model sayo = new Model(new Name("Sayo", "Aizawa"));
            Model asami = new Model(new Name("Asami", "Usuda"));

            Magazine cancam = new Magazine("CanCam");
            cancam.model.add(new Model(new Name("Yuri", "Ebihara")));
            cancam.model.add(new Model(new Name("Asami", "Usuda")));
            session.save(cancam);

            Magazine vivi = new Magazine("ViVi");
            vivi.model.add(new Model(new Name("Sayo", "Aizawa")));
            session.save(vivi);

            Magazine classy = new Magazine("Classy");
            classy.model.add(new Model(new Name("Sayo", "Aizawa")));
            session.save(classy);

            session.flush();

            session = factory.openSession();
            Iterator it = session.find("from study.Magazine").iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

こいつを実行!!

 onSave() : CanCam
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : ViVi
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : Classy
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 Hibernate: insert into Model (magazine, first_name, last_name) values (?, ?, ?)
 Hibernate: select magazine0_.id as id, magazine0_.name as name from Magazine magazine0_
 onLoad() : CanCam
 onLoad() : ViVi
 onLoad() : Classy
 Hibernate: select model0_.first_name as first_name__, model0_.last_name as last_name__, model0_.magazine as magazine__ from Model model0_ where model0_.magazine=?
 Hibernate: select model0_.first_name as first_name__, model0_.last_name as last_name__, model0_.magazine as magazine__ from Model model0_ where model0_.magazine=?
 Hibernate: select model0_.first_name as first_name__, model0_.last_name as last_name__, model0_.magazine as magazine__ from Model model0_ where model0_.magazine=?
 CanCam : [Yuri Ebihara, Asami Usuda]
 ViVi : [Sayo Aizawa]
 Classy : [Sayo Aizawa]

ふむ.なんかうまくいったっぽい.
今回のモデルは値型なので,雑誌を掛け持ちしている紗世ちゃんは2行できているはず.ということで MODEL テーブルを確認してみると...
ぐはぁっ,空っぽ.(;_;)
MAGAZINE テーブルも空っぽだぞ... どうしてぇ??


うーみゅ,HSQLDB を 1.7.2 に変更したのが原因のようです.実行中にちゃんと SELECT できているのですから,トランザクションがコミットされていないということでしょうか?
ということで,最初のセッションを flush() した後に次の行を追加.

            session.connection().commit();

そして実行!!!!
おぉ,今度はちゃんとDBが更新されました.\(^o^)/
意外なところに影響が出るものですね...
もしかすると,コミットしていないのに DB が更新されてしまったこれまで (1.7.1) がおかしかったのでしょうか? それとも 1.7.1 ではデフォルトで autoCommittrue だったけれど,1.7.2 では false に変わったとか? でも changelist にはそれらしきことは書いてないような.
ま,いっか.
これからはきっちりコミットしろってことですね.ちぇっ,9章に行くまでコミットもクローズもしないまま放置しようと思ったのにぃ.


ともあれ,これで7章は終了です.へへっ(えみちい風)
次は「8. Inheritance Mapping」.これまでスキップしまくってきた継承の登場です.ドキドキっ!
でも明日は EbiYuriデーで CanCam レポートもあるので,次回は来週かな... 心より恥じる.