Hibernate 入門記 コレクションその11 Lazy Initialization

長く続いたコレクション編の締めくくりは,「6.5. Lazy Initialization」です.遅延初期化というか遅延ロードというか.
これは,コレクション (関連) をデータベースからロードして永続クラス (または値型) のインスタンスを生成することを,本当に必要になるまで先延ばしするということです.当然,パフォーマンス向上のための機能なんでしょうね.しかし...
遅延初期化が本当にパフォーマンス向上につながるのか,疑問を感じないでもありません.ここは頭の切替が必要なようです.


従来,RDB のアクセスで良好なパフォーマンスを得るには,JOIN を活用することで SELECT の発行回数が最低限になるようにすると思います.繰り返しの中で SELECT を呼び出すようなことはしちゃいけないわけです.
また,不要なカラムを選択しないのもポイント (っていうか常識でしょうか).テーブルにカラムが50個合っても,必要なカラムが10個だけなら他の40個は取ってこないということです.
これらは,取得するリレーションの数を最低限にする,そしてそのリレーションを最小化するということですね.


しかし,このようなリレーションの最適化はオブジェクト指向 (というよりクラス) との相性がよろしくないということは,以前の「インピーダンス・ミスマッチ」でも書いた通り.
永続クラスを使うということは,そのとき必要かどうかにかかわらず,マッピングされた全てのカラムを取ってくるということであり,リレーションの最小化は果たせません.
また,関連のナビゲーションごとに SELECT を発行すると,リレーションの数 (問い合せの発行回数) を最低限にすることもできません.
このように,O/R マッピングを使うということは,RDB の性能を引き出すことがきわめて困難になると思えるわけです.


しかし,このような考え方はもしかすると古いのかもしれません.
もしかしてもしかすると,RDB のパフォーマンスを引き出すための,新しい考え方があるのかも?
その「新しい考え方」がキャッシングと遅延評価です.えっ? 全然新しくないって?(^^;
そう,パフォーマンス向上の手段といえばキャッシングに遅延評価と昔から相場が決まっているのです.
そして,O/R マッピングにおけるRDBアクセスのパフォーマンス向上は,リレーションの最適化ではなく,キャッシングと遅延評価による最適化で果たされることになるのかもしれない,と思ったり.
強引に例えると,リレーション最適化はオーバーレイ,キャッシングと遅延評価による最適化は仮想記憶かもしれない,みたいな.うわぁっ,石を投げないでぇ〜.


基本的な考え方は,キャッシングにより (新たに) 取得するリレーションの数 (問い合わせの発行回数) を最低限にする,そして遅延評価 (JOIN しない) により (その時点での) リレーションを最小限にする,ということになるのではないかと.方法は全然違うものの,リレーション最適化と同等のパフォーマンスを得られる可能性はある... のか?
個々の問い合せごとに最適なリレーション (結果セット) を考えようとすると (バリエーションが多いために) キャッシングの効率は悪化するでしょうから,永続クラスの決まりきった形 (カラムの構成) で問い合せをするのはキャッシングという点からは合理的なんだと考えてみたり.そうしてキャッシュが有効であれば,毎回 JOIN しないでキャッシュされていない必要最低限のリレーションだけをデータベースから取得するのは合理的なんだと考えてみたり.
課題は,トランザクションやセッション,さらには JVM をまたいで効果的なキャッシュを実現できるか? というところでしょう.実現できればいけるかもしれないし,実現できなければ満足のいくパフォーマンスは期待できないように思います.
そういう,トランザクショナルな分散キャッシュが実用レベルであれば,それは O/R マッピングにとっては鬼に金棒だと思いますが,現実は?


ちなみにトランザクショナルな分散キャッシュというと,実はオブジェクトデータベースっていうのはそういうしろものだったりします.ObjectStore なんかはその極めつけでした.しかし... これまでのところ,とてもじゃないけれど成功しているとはいえません.性能だけが理由ではないでしょうけれど,OLTP 用途で優れたパフォーマンスを発揮することがきわめて困難だったことは間違いなく,OLTP そのものといえる WEB アプリケーションでトランザクショナルな分散キャッシュを有効に使うこともかな〜り難しいのではないかと想像します.


ともあれ,Hibernate におけるキャッシュの解説は「14. Improving performance」あたりで解説されるみたいです.遠いな...


ということで,今回は遅延初期化の使い方だけ学習することにしましょう.有効に使えるかどうかは気にしない,気にしない.
遅延初期化を使えるのは,配列 (<array> 要素と <primitive-array> 要素) を除いたコレクションです.
遅延初期化したいコレクション (例えば <set> 要素) の lazy 属性に true を指定すればそれで終了です.簡単♪
そんなわけで前振りが長めだったわけです.頑張ってるでしょ? ボクぅ (谷川さん風)


おっと,遅延初期化が有効なのは,トランザクションの間だけらしいです.トランザクションをコミットした後に (ロードされていない) コレクションにアクセスすると例外が吹っ飛んでくるらしいので気をつけましょう.


ということで,さっそくお試しします.
最後でもあることだし,すっかり定番となったモデルと雑誌の関連 (セット) で遅延初期化を使います.
まずはテーブル定義.

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

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

CREATE TABLE MODEL_MAGAZINE (
    MODEL INTEGER,
    MAGAZINE INTEGER,
    PRIMARY KEY(MODEL, MAGAZINE)
)

よしよし,関連テーブルに ID を付けてないぞ.
そしてモデルの永続クラス.

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

    public Model() {
    }
    public Model(String name) {
        this.name = name;
        this.magazine = new HashSet();
    }

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

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

今回は,onLoad() 等の中で toString() を呼び出さないようにしています.でないと,そこでコレクションにアクセスしてしまうためです.お楽しみ (コレクションへのアクセス) は後に取っておかないと.
そしてモデルのマッピングファイル.まずは遅延初期化無効で.

<?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="name" access="field"/>
        <set name="magazine" access="field" table="MODEL_MAGAZINE" cascade="all" lazy="false">
            <key column="model"/>
            <many-to-many column="magazine" class="Magazine"/>
        </set>
    </class>
</hibernate-mapping>

次に雑誌の永続クラス.

package study;
import java.io.Serializable;
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;

    public Magazine() {
    }
    public Magazine(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;
    }
}

雑誌のマッピングファイル.

<?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"/>
    </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();

            Magazine cancam = new Magazine("CanCam");
            Magazine vivi = new Magazine("ViVI");
            Magazine classy = new Magazine("CLASSY");

            Model yuri = new Model("Yuri Ebihara");
            yuri.magazine.add(cancam);
            session.save(yuri);

            Model sayo = new Model("Sayo Aizawa");
            sayo.magazine.add(vivi);
            sayo.magazine.add(classy);
            session.save(sayo);

            Model asami = new Model("Asami Usuda");
            asami.magazine.add(cancam);
            session.save(asami);

            session.flush();

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

こいつを実行!! (SELECT 以降のみ抜粋)

 Hibernate: select model0_.id as id, model0_.name as name from Model model0_
 onLoad() : Yuri Ebihara
 onLoad() : Sayo Aizawa
 onLoad() : Asami Usuda
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : CanCam
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : ViVI
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : CLASSY
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Yuri Ebihara : [CanCam]
 Sayo Aizawa : [ViVI, CLASSY]
 Asami Usuda : [CanCam]

もはや見慣れた風景ですね.
次に,モデルのマッピングファイルを次のように変更して遅延初期化を有効にします.

        <set name="magazine" access="field" table="MODEL_MAGAZINE" cascade="all" lazy="true">

そして実行!!

 Hibernate: select model0_.id as id, model0_.name as name from Model model0_
 onLoad() : Yuri Ebihara
 onLoad() : Sayo Aizawa
 onLoad() : Asami Usuda
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : CanCam
 Yuri Ebihara : [CanCam]
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : ViVI
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 onLoad() : CLASSY
 Sayo Aizawa : [CLASSY, ViVI]
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Asami Usuda : [CanCam]

ふむふむ.
遅延初期化を有効にする前は,エビちゃんの出力の前に全ての問い合せが実行されて,雑誌も全てロードされていましたが,遅延初期化を有効にするとエビちゃんの出力以前は CanCam だけがロードされていて,ViVi や CLASSY はロードされていないことがわかります.また,個々の関連をたどるための問い合せも表示される直前になってから実行されています.
今回の場合は,最初から関連づけられた全ての雑誌にアクセスすることがわかっているため,遅延初期化を使ってもパフォーマンスの向上は図れません.それよりも,JOIN することで問い合わせを1回にする方が効果的です.
しかし,条件によっては雑誌にアクセスしない,しかもアクセスしない方が多くて,必要があってもキャッシングされていることが期待できるなんていう場合には,遅延初期化によるパフォーマンス向上が期待できるのかもしれません.


さて,こんなところでコレクション編終了です.たぶん.3項関連もやったしね.
いろいろなコレクションがありましたが,個人的にはセットとマップだけでいいかなぁという感じ.
リストはインデックスのカラムが必要なのが気に入らないので使わないと思います.
バッグは java.util.Bag がなくても困っていないくらいで,必要になることがほとんどありません.
ということで,セットとマップがあれば,きっと大丈夫♪


それから,懸案になっていた外部結合についても少しわかってきました.
Hibernate Users FAQ - Advanced Problems」によると,outer-join="true" 属性を設定した場合に Hibernate が勝手に外部結合してくれるのは,Session#load(Class, Serializable)Session#get(Class, Serializable) などを使った場合だけで,Session#find(String) などの場合には,以前やったように HQL で明示的に外部結合を指定しなければならないようです.
実際,自前の HSQLDialect を作成して試したところ,Session#load(Class, Serializable) ではちゃんと outer-join="true" に応じて外部結合してくれました.以前試していたときは,Session#find(String) しか使っていなかった (知らなかった) ので外部結合してくれないと悩んでいたわけです.無念だ.
でも,Session#load(Class, Serializable) ってそんなに使うのでしょうか? 第二引数の Serializable って主キー(ID プロパティ)の値なんですよね.この場合の主キーは業務的な意味を持たないものが推奨な訳ですから,最初に永続オブジェクトを取得する場合は HQL を使う方が多いのではないかと思うのですが... だとすると,outer-join 属性はそんなにうれしくないような.
それとも,ここでも発想を変える必要があるということでしょうか?


ともあれ,次回からは「7. Component Mapping」へ進みます.うーみゅ,またしてもチマチマとしか進めなさそうなタイトルだにゃあ.
セカ^h^hパフォチューへの道は遠い...