Hibernate 入門記 継承その1 table per class hierachy

今回からは「Chapter 8. Inheritance Mapping」へ進みます.
長いことスキップしまくってきた継承なのでドキドキしていたのですが,ドキュメントの量は意外なくらいに少なめ.もしかして簡単?
いやいや,薄ーい数学の本は易しいかと思いきや,丁寧な説明が全然なくて超難解だったりして,痛い目にあったことが何度もあるので油断大敵です.慎重に行かねば.
...
でも,やっぱりたいしたことなさげ.
8.1. The Three Strategies」によると,Hibernate では継承を次の3通りの方法でマッピングできるようです.

  • table per class hierarchy
  • table per subclass
  • table per concrete class

ふむ.EJB3.0 (Early Draft) と同じみたいですね.
そして「8.2. Limitations」にはそれぞれの制限が書いてあるようです.
なので,この3つを1つずつ片付けていけばよさげ♪


ということで,まずは table per class hierarchy です.
このマッピングは,継承階層全体を一つのテーブルにマッピングするというものです.単純というか乱暴というか.
そのテーブルには,あるエンティティ (行) がクラス階層中のどのクラスのインスタンスなのかを識別するためのカラムが必要です.これを discriminator カラムと呼ぶようです.discriminator は識別とか差別とか弁別とかそんな意味らしいです.でも「識別」だとキーみたいに見えちゃうので,英語のままで押し通すことにします.
あるサブクラスでのみ必要なカラムでも,一つしかないテーブルに押し込まなければならないので,NOT NULL 制約を付けることは出来ません.このマッピングを使う上での制限はそれくらいみたい.使いやすいかも.


マッピングの指定については,まずはお馴染みの

  • <class> 要素

の内容として,ID プロパティ (主キー) の定義の後,プロパティや関連の定義の前に discriminator カラムを指定するための

  • <discriminator> 要素

を記述します.
こいつは次の属性を持っています.

column
カラム名を指定します.省略すると class になるみたいです.
discriminator カラムが複数ある場合は,子要素として <column> 要素を (複数) 記述します.
type
discriminator カラムの型を指定します.
指定できる型は,stringcharacterintegerbyteshortbooleanyes_noture_false とのこと.デフォルトは string らしいです.
force
クラス階層のルート (Object ではなくて <class> 要素で指定されるクラス) として永続クラスのインスタンスを取得する (サブクラスのインスタンスも取得される) 場合に,discriminator カラムの値を問い合わせ条件に含める場合に true を指定します.デフォルトは false です.

DTD 的には他にもいくつか属性があるみたいですが,リファレンスに書いてあるのはこれだけ.他は使うことなさそうです.


そして,<class> 要素の内容にクラス階層全体で共通となる (基底クラスの) プロパティや関連の定義を記述します.
それに続けて,個々のサブクラスのマッピングを指定する

  • <subclass> 要素

を記述します.
こいつは次の属性を持っています.

name
クラス名を指定します.必須です.
discriminator-value
このサブクラスを識別する discriminator カラムの値を指定します.省略するとクラス名 (完全限定名) が使われます.
proxy
遅延初期化する場合に指定するようですが,関連以外の遅延初期化はまだ学習していません.無念だ.
lazy
同上.
dynamic-update
すでにお馴染み.永続オブジェクトを更新した際に,変更のあったプロパティ (カラム) だけを更新するような SQL を発行する場合に true を指定します.デフォルトは false です.
dynamic-insert
上記の INSERT 版.

そしてこいつの内容には <class> 要素と同じようにプロパティや関連のマッピングを記述します.
それに続けて,<subclass> 要素も記述することが出来ます.深い深ーいクラス階層も記述できるのですね.


クラス階層の各クラスにおいて,インスタンスを生成できるのは派生クラスだけとは限りません.一番上 (ルート) の基底クラスのインスタンスも生成できるかもしれません.そのため,ルートクラスのインスタンスであることを識別する discriminator カラムの値を <class> 要素の discriminator-value 属性で指定します.


予習はこれくらいかな.後は実践あるのみ!
例によってネタに困るわけですが,今回はタレントのサブクラスとしてモデルと女優を作ってみます.
え? モデルが女優になったらどうするって? 兼任の人はどうするって?
いいんです,そんな細かいこと考えてたら何も出来なくなっちゃいますよ.
それで,モデルの discriminator カラムの値は "M",女優は "A",モデルでも女優でもないタレントは "T" ということにします.
そして,モデルには雑誌,女優にはドラマというプロパティがあることにしましょう.
ということでテーブル定義.

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

CLASS というカラムが discriminator カラムです.
そしてまずは基底クラスであるタレントの永続クラス.

package study;
import java.io.Serializable;
import net.sf.hibernate.CallbackException;
import net.sf.hibernate.Lifecycle;
import net.sf.hibernate.Session;

public class Talent implements Lifecycle {
    int id = -1;
    String name;

    public Talent() {
    }
    public Talent(String name) {
        this.name = name;
    }

    public String toString() {
        return name;
    }

    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;
    }
}

特にどうということもなく.
次にモデルの永続クラス.

package study;

public class Model extends Talent {
    String magazine;

    public Model() {
    }
    public Model(String name, String magazine) {
        super(name);
        this.magazine = magazine;
    }

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

そして女優の永続クラス.

package study;

public class Actress extends Talent {
    String drama;

    public Actress() {
    }
    public Actress(String name, String drama) {
        super(name);
        this.drama = drama;
    }
    public String toString() {
        return name + " : " + drama;
    }
}

そしてタレントをルートとするクラス階層のマッピングファイル,study/Talent.hbm.xml です.

<?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="Talent" discriminator-value="T">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <discriminator column="class" type="string"/>
        <property name="name" access="field"/>

        <subclass name="Model" discriminator-value="M">
            <property name="magazine" access="field"/>
        </subclass>

        <subclass name="Actress" discriminator-value="A">
            <property name="drama" access="field"/>
        </subclass>
    </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("Yuri Ebihara", "CanCam");
            session.save(yuri);

            Actress akiko = new Actress("AKiko Yada", "My Little Chef");
            session.save(akiko);

            Talent kyoko = new Talent("Kyoko Uchida");
            session.save(kyoko);

            session.flush();
            session.connection().commit();

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

ウッチーかよ! とか 女子アナかよ! っていう突っ込みは勘弁してください.心より恥じる.
ともかく,こいつを実行!!!!

 onSave() : Yuri Ebihara
 Hibernate: insert into Talent (magazine, name, class, id) values (?, ?, 'M', null)
 Hibernate: CALL IDENTITY()
 onSave() : AKiko Yada
 Hibernate: insert into Talent (drama, name, class, id) values (?, ?, 'A', null)
 Hibernate: CALL IDENTITY()
 onSave() : Kyoko Uchida
 Hibernate: insert into Talent (name, class, id) values (?, 'T', null)
 Hibernate: CALL IDENTITY()
 Hibernate: select talent0_.id as id, talent0_.class as class, 
         talent0_.name as name, talent0_.magazine as magazine, talent0_.drama as drama 
     from Talent talent0_
 onLoad() : Yuri Ebihara
 onLoad() : AKiko Yada
 onLoad() : Kyoko Uchida
 Yuri Ebihara : CanCam
 AKiko Yada : My Little Chef
 Kyoko Uchida

おおぉぉぉぉっ,楽勝っぽい.
INSERT の SQL を見ても意図した通りみたいだし,テーブルの中身も大丈夫.やったね!


次にマッピングファイルの <discriminator> 要素の force 属性を true にしてみました.
そして実行!! (SELECT のみ抜粋)

 Hibernate: select talent0_.id as id, talent0_.class as class, 
         talent0_.name as name, talent0_.magazine as magazine, talent0_.drama as drama 
     from Talent talent0_ where talent0_.class in ('A', 'T', 'M')

と,WHERE 句が付加されました.これで, discriminator カラムの値がマッピングで指定されたものだけ選択されるように強制するという事みたいです.
では,force 属性を false にして,discriminator カラムの値がマッピング定義にない値 ("X") があったらどうなるのか,試してみました.
すると...

 net.sf.hibernate.WrongClassException: Object with id: 0 was not of the specified subclass: 
         study.Talent (Discriminator: X)
     at net.sf.hibernate.loader.Loader.getInstanceClass(Loader.java:581)
     at net.sf.hibernate.loader.Loader.instanceNotYetLoaded(Loader.java:494)
     at net.sf.hibernate.loader.Loader.getRow(Loader.java:426)
     at net.sf.hibernate.loader.Loader.doQuery(Loader.java:209)
     at net.sf.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:133)
     at net.sf.hibernate.loader.Loader.doList(Loader.java:955)
     at net.sf.hibernate.loader.Loader.list(Loader.java:946)
     at net.sf.hibernate.hql.QueryTranslator.list(QueryTranslator.java:846)
     at net.sf.hibernate.impl.SessionImpl.find(SessionImpl.java:1543)
     at net.sf.hibernate.impl.SessionImpl.find(SessionImpl.java:1520)
     at net.sf.hibernate.impl.SessionImpl.find(SessionImpl.java:1512)
     at study.Main.main(Main.java:28)

まぁ,当然ですかね.
ちょっと分かりにくかった <discriminator> 要素の force 属性はこんな意味らしいです.discriminator カラムにおかしな値が入っていた場合のガードになりそうですが,おかしな値が入る方をどうにかすべきですよね.どんな場合に使うのでしょう? レガシーなテーブル? それで継承を使うとも思えないし... ???
まぁいっか.


ということで,今日はここまで.
次回は table per subclass です.