Hibernate入門記 マッピング定義その6 one-to-one要素

前回の問題が未だに片づいていないのですが,とりあえず前進し続けます.
今回は「5.1.11. one-to-one」,1対1の関連を表す

  • <one-to-one> 要素

です.
使い方は <many-to-one> そっくりで,columnupdateinsertといった属性が使えなくて,代わりに以下の属性を指定することができます.

constrained
外部キー制約が有効な場合に true を指定します.デフォルトは false です.cascade 属性が "none" 以外の場合に,INSERTやDELETEをする順番に影響があります.

という具合に簡単そうなんですが,1対1関連の表現には二通りあるとのこと.
一つめの方法は,二つのテーブルが同じ主キーの値を持つというもの.この場合,関連のためによけいなカラムを持つ必要はありません.この場合は,<id> 要素のジェネレータ(覚えてますか?)に foreign を使えるとのこと.
もう一つの方法は,外部キーを使うというもの.しかしですね,外部キーとなるカラムを指定する方法がわかりません... 無念だ.
とうのもですね,リファレンスの例がどういうわけか <many-to-one> になっちゃってるんですよ.そこでは column 属性を使っているのですが,<one-to-one> には column 属性はありません.無念だ.念のためDTDを見てみると...
ぐはぁっ,foreign-key 属性なんてあるし.あう,<many-to-one> 要素にもありますね... 油断していたなぁ,これまで学習した要素にもリファレンスで説明されていない属性がチラホラあるっぽい.
ともあれ,その名もズバリなので <foreign-key> 属性で外部キーとなるカラムを指定するのだと仮定しておきましょう.


その仮定を検証すべく,実験開始です.
しかし困りました.雑誌モデルと1対1関連があるようなものって? レギュラー番組も出演中のCMも2本あるので1対1ではありません.どうしよう?
そんなことで悩んでいてもしょうがないので,CanCamモデルに絞ってレギュラーとなっているコーナーを関連づけてみましょう.
それから,<many-to-one> の実験コーナーで試したのは多の側から1の側への単方向の関連でした.逆方向の関連は1対多になりますが,それはまだ学習していないので無理だったのです.しかーし! 1対1の逆もまた1対1ですから,今回は双方向の関連を試してみましょう.
ということでテーブル定義ですが,まずは同じ主キー値を使う方法から試してみます.

CREATE TABLE MODEL (
    ID INTEGER IDENTITY,
    FIRSTNAME VARCHAR,
    LASTNAME VARCHAR
)

CREATE TABLE SERIES (
    ID INTEGER,
    TITLE VARCHAR
)

面倒なので誕生日や専属の雑誌を外してしまいました.
次にモデルさんのクラス.

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

public class Model implements Lifecycle {
    int id = -1;
    String firstName;
    String lastName;
    Series series;

    public String toString() {
        return firstName + " " + lastName + ", " + "'" + series.title + "'";
    }

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

そして連載のクラス.

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

public class Series implements Lifecycle {
    int id = -1;
    String title;
    Model model;

    public String toString() {
        return "'" + title + "', " + model.firstName + " " + model.lastName;
    }

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

モデルさんのマッピングファイル,study/Model.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="Model">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="firstName" access="field"/>
        <property name="lastName" access="field"/>
        <one-to-one name="series" access="field" cascade="all"/>
    </class>
</hibernate-mapping>

そして連載のマッピングファイル,study/Series.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="Series">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="foreign">
                <param name="property">model</param>
            </generator>
        </id>

        <property name="title" access="field"/>
        <one-to-one name="model" access="field" constrained="true"/>
    </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/Series.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();

            Series ebiOL = new Series();
            ebiOL.title = "かわいい系エビちゃんOL";

            Model yuri = new Model();
            yuri.firstName = "Yuri";
            yuri.lastName = "Ebihara";
            yuri.series = ebiOL;
            ebiOL.model = yuri;
            session.save(yuri);

            Series yuOL = new Series();
            yuOL.title = "キレイ系優OL";

            Model yu = new Model();
            yu.firstName = "Yu";
            yu.lastName = "Yamada";
            yu.series = yuOL;
            yuOL.model = yu;
            session.save(yu);

            Series nareMoe = new Series();
            nareMoe.title = "なれるものなら押切もえ";

            Model moe = new Model();
            moe.firstName = "Moe";
            moe.lastName = "Oshikiri";
            moe.series = nareMoe;
            nareMoe.model = moe;
            session.save(moe);

            session.flush();

            session = factory.openSession();

            Iterator it = session.find("from study.Model").iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

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

データを作った後,モデルと連載それぞれを検索してプリントしてます.
これを実行!

・・・
 Hibernate: select model0_.id as id, model0_.firstName as firstName, model0_.lastName as lastName from Model model0_
 Hibernate: select series0_.id as id0_, series0_.title as title0_ from Series series0_ where series0_.id=?
 onLoad() : 'かわいい系エビちゃんOL', null null
 onLoad() : Yuri Ebihara, 'かわいい系エビちゃんOL'
 Hibernate: select series0_.id as id0_, series0_.title as title0_ from Series series0_ where series0_.id=?
 onLoad() : 'キレイ系優OL', null null
 onLoad() : Yu Yamada, 'キレイ系優OL'
 Hibernate: select series0_.id as id0_, series0_.title as title0_ from Series series0_ where series0_.id=?
 onLoad() : 'なれるものなら押切もえ', null null
 onLoad() : Moe Oshikiri, 'なれるものなら押切もえ'
 Yuri Ebihara, 'かわいい系エビちゃんOL'
 Yu Yamada, 'キレイ系優OL'
 Moe Oshikiri, 'なれるものなら押切もえ'
 Hibernate: select series0_.id as id, series0_.title as title from Series series0_
 'かわいい系エビちゃんOL', Yuri Ebihara
 'キレイ系優OL', Yu Yamada
 'なれるものなら押切もえ', Moe Oshikiri

ふむ.結果はおっけー.相変わらずSELECTがネストしてますが... 心より恥じる.
連載を取得するためにSELECTした後,モデルに対するSELECT,モデル・連載の onLoad() が行われていませんね.


ということで,次に外部キーを使った場合を試してみます.
まずはテーブル定義.MODELテーブルは同じですが,SERIESテーブルを次のように変更します.

CREATE TABLE SERIES (
    ID INTEGER IDENTITY,
    TITLE VARCHAR,
    MODEL INTEGER
)

モデルと連載のクラスはともに変更なし.
そしてモデルさんのマッピングファイル,study/Model.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="Model" batch-size="10">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="firstName" access="field"/>
        <property name="lastName" access="field"/>
        <one-to-one name="series" access="field" cascade="all" property-ref="model"/>
    </class>
</hibernate-mapping>

そして連載のマッピングファイル,study/Series.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="Series">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="title" access="field"/>
        <one-to-one name="model" access="field" foreign-key="MODEL"/>
    </class>
</hibernate-mapping>

hibernate.cfg.xml と実行用のクラスは変更なし.
これを実行!!

・・・
 net.sf.hibernate.MappingException: broken column mapping for: series.model of: study.Model
     at net.sf.hibernate.persister.AbstractPropertyMapping.initPropertyPaths(AbstractPropertyMapping.java:88)
     at net.sf.hibernate.persister.AbstractPropertyMapping.initIdentifierPropertyPaths(AbstractPropertyMapping.java:123)
     at net.sf.hibernate.persister.AbstractPropertyMapping.initPropertyPaths(AbstractPropertyMapping.java:104)
     at net.sf.hibernate.persister.AbstractPropertyMapping.initPropertyPaths(AbstractPropertyMapping.java:80)
     at net.sf.hibernate.persister.AbstractEntityPersister.initPropertyPaths(AbstractEntityPersister.java:514)
     at net.sf.hibernate.persister.EntityPersister.postInstantiate(EntityPersister.java:110)
     at net.sf.hibernate.impl.SessionFactoryImpl.(SessionFactoryImpl.java:156)
     at net.sf.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:768)
     at study.Main.main(Main.java:12)

ぐはぁっ,そんなに甘くはなかったか... 無念だ.
うーみゅ,属性を付けたり外したり,いろいろ試行錯誤してみたのですが,どうにもうまくいきません.
リファレンス通り,Series の側を <many-to-one> にするとあっさりと動くのですが...
果たして外部キーを使用した双方向の1対1関連はサポートされているのでしょうか? 1対1であれば外部キーを使う必然性はなさそうなので,サポートされていなくても構わない気もしますが,なんか悔しい.
前回に続いてまたしても中途半端ではあるのですが,もしかしたら再び id:nekop さんが何かヒントをくださるかもしれないので,一端ここで終了とします.
心より恥じる.