Hibernate 入門記 番外編 外部結合まとめ

某巨大掲示板に この日記の URL が貼られてました.かなりドキドキ.
また写真が増えておるよ.このサイト.
そんなわけで (どんなわけで?),Hibernate の問い合わせと外部結合について軽くまとめてみるテスト.


話題になっているのは多対 1 の問い合わせ.この日記的にはモデルと雑誌みたいな.
ということで,マッピングファイルだけ掲載.
まずはモデル.

<?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" default-access="field">
    <class name="Model" lazy="false">
        <id name="id" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <property name="name"/>
        <many-to-one name="magazine" class="Magazine"/>
    </class>
</hibernate-mapping>

そして雑誌.

<?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" default-access="field">
    <class name="Magazine">
        <id name="id" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <property name="name"/>
    </class>
</hibernate-mapping>

これを次のように問い合わせします.

Iterator it = 
    session.find("from study.Model model where model.name='Yuri Ebihara'").iterator();

すると...

Hibernate: select 
               model0_.id as id, model0_.name as name, model0_.magazine as magazine 
           from 
               Model model0_ 
           where 
               (model0_.name='Yuri Ebihara' )
Hibernate: select 
               magazine0_.id as id0_, magazine0_.name as name0_ 
           from 
               Magazine magazine0_ 
           where 
               magazine0_.id=?
Yuri Ebihara (CanCam)

まずはモデルだけが問い合わせされて,その後雑誌への問い合わせが行われます.
outer-join="true" を指定しても,勝手に外部結合はしてくれませんから!! 残念!!!!


じゃあ outer-join="true" はなんなのかというと,find() ではなく,load()/get() を使った場合に効果があります.

Model model = (Model) session.get(Model.class, new Integer(1));

この場合だと,

Hibernate: select 
               model0_.id as id1_, model0_.name as name1_, model0_.magazine as magazine1_, 
               magazine1_.id as id0_, magazine1_.name as name0_ 
           from 
               Model model0_ 
                   left outer join Magazine magazine1_ on model0_.magazine=magazine1_.id 
           where 
               model0_.id=?
Yuri Ebihara (CanCam)

外部結合してくれました.
outer-join="false" を指定すると外部結合しなくなります.


でもでも,load()/get() を使えるのは ID プロパティ (主キー) による問い合わせのみ.最初の例のように,任意のプロパティを指定した問い合わせには使えませんから!! 残念!!!!
そこで,明示的な外部結合が必要になります.

Iterator it = 
    session.find(
        "from study.Model model left outer join model.magazine " 
            + "where model.name = 'Yuri Ebihara'").iterator();

その結果.

Hibernate: select 
               model0_.id as id0_, magazine1_.id as id1_, model0_.name as name0_, 
               model0_.magazine as magazine0_, magazine1_.name as name1_ 
           from 
               Model model0_ 
                   left outer join Magazine magazine1_ on model0_.magazine=magazine1_.id 
           where 
               (model0_.name='Yuri Ebihara' )
[Yuri Ebihara (CanCam), CanCam]

結合してくれるのはいいのですが,結果が配列で返って来ちゃうのが難点.
そこで,FETCH JOIN を使ってみましょう.

Iterator it = 
    session.find(
        "from study.Model model left outer join fetch model.magazine " 
            + "where model.name = 'Yuri Ebihara'").iterator();

その結果.

Hibernate: select 
               model0_.id as id0_, magazine1_.id as id1_, model0_.name as name0_, 
               model0_.magazine as magazine0_, magazine1_.name as name1_ 
           from 
               Model model0_ 
                   left outer join Magazine magazine1_ on model0_.magazine=magazine1_.id 
           where 
               (model0_.name='Yuri Ebihara' )
Yuri Ebihara (CanCam)

バッチリ!! でしょ?>掲示板の人


今回,FETCH JOIN で話がすんだのは多対1だからです.
1対多だと,多の側の数だけ1の方が返って来ちゃいます.
ということで,さっきはモデルから雑誌への1対多だった関連を,今度は雑誌からモデルへの1対多に変更します.
マッピングファイルのみ掲載.まずは雑誌.

<?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" default-access="field">
    <class name="Magazine">
        <id name="id" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <property name="name"/>
        <set name="model">
            <key column="magazine"/>
            <one-to-many class="Model"/>
        </set>
    </class>
</hibernate-mapping>

そしてモデル.

<?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" default-access="field">
    <class name="Model" lazy="false">
        <id name="id" unsaved-value="-1">
            <generator class="identity"/>
        </id>
        <property name="name"/>
    </class>
</hibernate-mapping>

でもって,次の問い合わせ.

Iterator it = 
    session.find(
        "from study.Magazine magazine left outer join fetch magazine.model model " 
            + "where magazine.name = 'CanCam'").iterator();

その結果.

Hibernate: select 
               magazine0_.id as id0_, model1_.id as id1_, magazine0_.name as name0_, 
               model1_.name as name1_, model1_.magazine as magazine__, model1_.id as id__ 
           from 
               Magazine magazine0_ 
                   left outer join Model model1_ on magazine0_.id=model1_.magazine 
           where 
               (magazine0_.name='CanCam' )
CanCam [Yuri Ebihara, Asami Usuda]
CanCam [Yuri Ebihara, Asami Usuda]

ということで,関連するモデルの数 (2件) だけ雑誌が返って来ちゃうのです.残念!!!!


これに対処する方法のひとつはクリテリアを使うこと.

Iterator it = session
    .createCriteria(Magazine.class)
    .add(Expression.eq("name", "CanCam"))
    .setFetchMode("model", FetchMode.EAGER)
    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
    .list().iterator();

その結果.

Hibernate: select 
               this.id as id1_, this.name as name1_, model1_.magazine as magazine__, 
               model1_.id as id__, model1_.id as id0_, model1_.name as name0_ 
           from 
               Magazine this 
                   left outer join Model model1_ on this.id=model1_.magazine 
           where 
               this.name=?
CanCam [Asami Usuda, Yuri Ebihara]

バッチリ!!


でもでも,クリテリアを使うと問い合わせの外部化が出来ないんですよね.可読性もイマイチだし.
そこで,Iterator のラッパーを作ってみました.

package study;

import java.util.Iterator;
import java.util.NoSuchElementException;

public class DistinctIterator implements Iterator {
    private Iterator iterator;
    private Object prev;
    private Object next;

    public DistinctIterator(Iterator iterator) {
        this.iterator = iterator;
    }

    public boolean hasNext() {
        if (prev == null) {
            return iterator.hasNext();
        }
        if (next != null) {
            return true;
        }
        while (iterator.hasNext()) {
            next = iterator.next();
            if (next != prev) {
                return true;
            }
        }
        next = null;
        return false;
    }

    public Object next() {
        if (prev == null) {
            prev = iterator.next();
            return prev;
        }
        if (next != null || hasNext()) {
            prev = next;
            next = null;
            return prev;
        }
        throw new NoSuchElementException();
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

テストファーストはもちろん,単体テストさえしていないので,バグバグの可能性もあると思いますが... 心より恥じる.
こいつを使った問い合わせ.

Iterator it = new DistinctIterator(session.find(
    "from study.Magazine magazine left outer join fetch magazine.model model "
        + "where magazine.name = 'CanCam'")
            .iterator());

そして実行!!!!

Hibernate: select 
               magazine0_.id as id0_, model1_.id as id1_, magazine0_.name as name0_, 
               model1_.name as name1_, model1_.magazine as magazine__, model1_.id as id__ 
           from 
               Magazine magazine0_ 
                   left outer join Model model1_ on magazine0_.id=model1_.magazine 
           where 
               (magazine0_.name='CanCam' )
CanCam [Asami Usuda, Yuri Ebihara]

バッチリ!!