双方向で 1 対 1 関連の Lazy フェッチ

id:da-yoshi さんのところでいろいろやっておられます.その
http://d.hatena.ne.jp/da-yoshi/20060131/1138664559
にコメントしている間に,
http://d.hatena.ne.jp/da-yoshi/20060201/1138721694
が書き込まれていて,なんか複雑なことになってるなぁ,と.(^^;
まぁ,元々 Hibernate 2.x の <one-to-one> というのは主キー同士で関連づけている場合の 1 対 1 を表すもので,同じ 1 対 1 でも外部キーを使う場合は <many-to-one> を使うことになっていたのでおかしなやり方ではないのですが.


以下,da-yoshi さんへのコメントを元に加筆修正です.
そもそも対 1 関連の Lazy フェッチというのは対多関連よりも複雑です.
対多関連の場合はコレクションを用意して,それがアクセスされた時に Lazy フェッチすればいいわけです.簡単.
しかし対 1 関連の場合はコレクションのように間接的なものはなく,関連先のインスタンスへの参照が必要となります.
ここで問題は二つ.一つは関連先のインスタンスをエンハンスしたプロキシを使って Lazy フェッチ可能にしなければならないこと.
もう一つは関連が存在しない場合はプロキシを参照に設定すること自体が許されないこと.null にしておかなきゃいけない.


一つめに関しては,エンティティクラスをエンハンスしてもらえばいいだけなのですが,Hibernate の場合それは明示的に指定する必要があります.
それが <class> 要素の lazy="true" です.
Persistence API には該当するアノテーションがないようで,Hibernate Annotations では独自の @org.hibernate.annotations.Proxy というアノテーションを用意しています.


んで,やっかいなのは二つめ.
RDB で対1関連を表すやり方の一つは外部キーを使うことですが,通常外部キーを双方向に使うことはありません.Foo と Bar が 1 対 1 である場合,例えば Bar 側に外部キーを持たせるなら Foo の側には外部キーは持たせません.
それを Java 側で双方向の 1 対 1 にマッピングすると困ったことになります.Bar をロードした場合はその外部キーを見れば Foo への参照を null にするかプロキシを設定するか判断できますが,Foo をロードする場合に Foo のテーブルだけを見てもその判断ができないのです.
外部キーではなく,主キーが同じ値を持つ二つのテーブルの場合はどちら向きでもこの問題に直面します.
そこで取りうる選択肢の一つは,Foo のロード時に Bar と外部結合して関連の有無を確認することですが,それだったら Lazy フェッチしない方がマシでしょう.


そんなわけで (どんなわけで?),外部キーを伴わない 対 1 関連は基本的に Lazy フェッチされません.
ただし.
同じ対 1 でも,多重度が 0..1 ではなく,厳密に 1,つまり 1..1 であれば関連先の有無を確認することなく,参照に対して無条件にプロキシを設定できるため,Lazy フェッチが可能になります.
それを指定するのが <one-to-one> 要素の constrained="true" です.


このような悩みは必ずしも Hibernat に限ったことではなく,おそらく O/R マッピングでは共通した悩みであると思われます.
そんなわけで (どんなわけで?),Java Persistence API でも @OneToOne アノテーションoptional=false を指定することができます.
意味合いが Hibernate とは逆なので注意.
Hibernate のは NOT NULL かどうかで,JPA のは NULLABLE かどうか.


ともあれ (JW),Hibernate でも (おそらく JPA でも) 双方向での 1 対 1 で Lazy フェッチは可能です.
そのためには,外部キーを持っているか,多重度が 1..1 であるか,どちらかである必要があります.


そんなわけで (どんなわけで?),主キー同士で関連づけられた場合のみやってみました.
エンティティクラスは省略.da-yoshi さんとこの Test1 と Test2 を参照.
まずは親側のマッピング

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC 
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping >
    <class name="hibernate.Test3" table="Test3" lazy="true">
        <id name="id">
            <generator class="identity"/>
        </id>
        <timestamp name="version"/>
        <property name="name"/>
        <one-to-one name="test4" constrained="true"/>
    </class>
</hibernate-mapping>

重要なポイントは <class> 要素に付けられた lazy="true" 属性と <one-to-one> 要素に付けられた constrained="true" 属性.


そして子側のマッピング

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC 
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping >
    <class name="hibernate.Test4" table="Test4" lazy="true">
        <id name="id">
            <generator class="foreign">
                <param name="property">test3</param>
            </generator>
        </id>
        <timestamp name="version"/>
        <property name="name"/>
        <one-to-one name="test3" constrained="true"/>
    </class>
</hibernate-mapping>

こちらも <class> 要素の lazy="true"<one-to-one> 要素の constrained="true" に注目.


実行したコードはこんな感じ.

        Test3 test3 = new Test3();
        test3.setName("TEST3");

        Test4 test4 = new Test4();
        test4.setName("TEST4");
        test4.setTest3(test3);
        test3.setTest4(test4);

        session.save(test3);
        session.save(test4);
        session.flush();
        session.clear();

        test3 = (Test3) session.get(Test3.class, (Serializable) test3.getId());
        System.out.println("TEST3.ID:" + test3.getId());
        System.out.println("TEST3.NAME:" + test3.getName());
        System.out.println("TEST3.VERSION:" + test3.getVersion());
        System.out.println();

        test4 = test3.getTest4();
        System.out.println("TEST4.ID:" + test4.getId());
        System.out.println("TEST4.NAME:" + test4.getName());
        System.out.println("TEST4.VERSION:" + test4.getVersion());
        System.out.println();

        session.clear();

        test4 = (Test4) session.get(Test4.class, (Serializable) test4.getId());
        System.out.println("TEST4.ID:" + test4.getId());
        System.out.println("TEST4.NAME:" + test4.getName());
        System.out.println("TEST4.VERSION:" + test4.getVersion());
        System.out.println();

        test3 = test4.getTest3();
        System.out.println("TEST3.ID:" + test3.getId());
        System.out.println("TEST3.NAME:" + test3.getName());
        System.out.println("TEST3.VERSION:" + test3.getVersion());
        System.out.println();

da-yoshi さんのとだいたい同じ.


で,その実行結果.

Hibernate: insert into Test3 (version, name, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Test4 (version, name, id) values (?, ?, ?)
Hibernate: select 
               test3x0_.id as id0_0_, test3x0_.version as version0_0_, test3x0_.name as name0_0_ 
           from 
               Test3 test3x0_ 
           where 
               test3x0_.id=?
TEST3.ID:7
TEST3.NAME:TEST3
TEST3.VERSION:2006-02-01 02:31:12.25

TEST4.ID:7
Hibernate: select 
               test4x0_.id as id1_0_, test4x0_.version as version1_0_, test4x0_.name as name1_0_ 
           from 
               Test4 test4x0_ 
           where 
               test4x0_.id=?
TEST4.NAME:TEST4
TEST4.VERSION:2006-02-01 02:31:12.312

Hibernate: select 
               test4x0_.id as id1_0_, test4x0_.version as version1_0_, test4x0_.name as name1_0_ 
           from 
               Test4 test4x0_ 
           where 
               test4x0_.id=?
TEST4.ID:7
TEST4.NAME:TEST4
TEST4.VERSION:2006-02-01 02:31:12.312

TEST3.ID:7
Hibernate: select 
               test3x0_.id as id0_0_, test3x0_.version as version0_0_, test3x0_.name as name0_0_ 
           from 
               Test3 test3x0_ 
           where 
               test3x0_.id=?
TEST3.NAME:TEST3
TEST3.VERSION:2006-02-01 02:31:12.25

きれいに外部結合していることがお分かりかと.


ただし,親 (Test3) 側の関連に cascade="all" を付けることができませんでした.
付けると子 (Test4) 側が先に INSERT されて,外部キー制約違反になっちゃうんですよね...
まぁ,1 対 1 で,しかも 1..1 ならカスケードにできなくても困らない気はしますが.
この辺は後日再調査するかも.しないかも.