パフォチュー その3 二次キャッシュ (read-only)

ついにパフォチューの本命,「14.3. The Second Level Cache」へ突入です.二次キャッシュですよ,二次キャッシュ.


二次キャッシュとはなんぞや? 2番目のキャッシュである.
1 番目はセッション.これはトランザクションレベルのキャッシュ.そして,他のセッションとは共有されないキャッシュ.
一方の 二次キャッシュは,トランザクションをまたがり,複数のセッション間で共有されるキャッシュ.
Web アプリなんかで,リクエストの度に毎度毎度アクセスされるような情報は,セッションレベルのキャッシュでは効果が期待できません.しかし,うまく二次キャッシュが使えれば,そのような情報も効果的にアクセスできます.もとい,アクセスをしないですませられます.


その二次キャッシュですが,永続クラスあるいはコレクションごとに設定をすることが出来るようです.
また,二次キャッシュの実装は自由に選択できるようです.それには,hibernate.cache.provider_class というプロパティで

  • CacheProvier

という interfaceimplements したクラスを指定します.
標準で用意されている CacheProvider の実装には,次のものがあるようです.

HashtableCacheProvider
文字通り,java.util.Hashtable を使って実装されたキャッシュを提供します.製品レベルでの利用は意図していないそうです.
EhCacheProvider
EHCache を使った実装を提供します.
OSCacheProvider
OSCache を使った実装を提供します.
SwarmCacheProvider
SwarmCache を使った実装を提供します.
クラスタ環境で使うことが出来るようです.
TreeCacheProvider
TreeCache を使った実装を提供します.
クラスタ環境で使うことが出来るようです.
トランザクショナルなキャッシュらしいです.

その他,CacheProvier さえ用意すれば,様々なキャッシュの実装を利用することが出来ます.以前この日記で紹介した TANGOSOL の Coherence を使うための CacheProvider が「Using Tangosol Coherence Cache」で紹介されています.
...
でも,これらのキャッシュをどう使い分けたらいいのかとかって何も書いてないんですよね... 残念!!!!
まぁ,個人的にはトランザクショナルでないキャッシュにはあまり興味がないので,使い分けは別にいいかなぁ.
次行きますよ! 次,次!! (エビちゃん風)


ということで「14.3.1. Cache mappings」へ進みます.
二次キャッシュを利用するには,マッピングファイルで指定をしなければいけないようです.それには,<class> 要素あるいは <set> 要素などの子として,

  • <cache> 要素

を記述します.
こいつは必須の属性を一つだけ持っています.

usage
キャッシング戦略を指定します.

キャッシング戦略として指定できる値は 4 つあります.

    • read-only
    • read-write
    • nonstrict-read-write
    • transactional

あるいは,hibernate.cfg.xml で,<session-factory> 要素の子として

  • <class-cache> 要素
  • <collection-cache> 要素

を記述してもいいようです.
ところで,キャッシング戦略ってどういうものよ?


そんなわけで (どんなわけで?),「14.3.2. Strategy: read only」です.
read only なキャッシング戦略は,文字通り更新されない情報をキャッシュするというもので,クラスタ環境でも安全に使うことが出来るそうです.
そうだねぇ,更新されない情報に限れば,キャッシングは単純だよねぇ.
あら? 説明はそれだけっすか?


ということで,お試ししましょう.
まずはテーブル.例によって例のネタで.

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

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

雑誌の永続クラス.

package study;

import java.util.HashSet;
import java.util.Set;

public class Magazine {
    int id = -1;
    String name;
    Set model;

    public Magazine() {
    }

    public Magazine(String name) {
        this.name = name;
        this.model = new HashSet();
    }

    public String toString() {
        return name + model;
    }
}

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

<?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" cascade="all">
            <key column="magazine"/>
            <one-to-many class="Model"/>
        </set>
    </class>
</hibernate-mapping>

最初はキャッシュの指定なしです.
モデルの永続クラス.

package study;

public class Model {
    int id = -1;
    String name;

    public Model() {
    }

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

    public String toString() {
        return name;
    }
}

モデルのマッピングファイル.

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

こいつも最初はキャッシングなし.
でもってデータ作成クラス.

package study;
import net.sf.hibernate.Session;

public class Save {
    public static void main(String[] args) {
        try {
            HibernateTemplate template = new HibernateTemplate();

            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Magazine cancam = new Magazine("CanCam");
                    session.save(cancam);
                    Magazine vivi = new Magazine("ViVi");
                    session.save(vivi);

                    cancam.model.add(new Model("Yuri Ebihara"));
                    cancam.model.add(new Model("Asami Usuda"));
                    vivi.model.add(new Model("Sayo Aizawa"));
                    vivi.model.add(new Model("Jun Hasegawa"));

                    return null;
                }
            });
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

こいつを実行しておきます.
そして問い合わせのクラス.

package study;

import java.util.Iterator;

import net.sf.hibernate.Session;

public class Query {
    public static void main(String[] args) {
        try {
            HibernateTemplate template = new HibernateTemplate();
            HibernateCallback printAll = new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Iterator it = session.find("from study.Magazine")
                            .iterator();
                    while (it.hasNext()) {
                        System.out.println(it.next());
                    }
                    return null;
                }
            };
            System.out.println("\n*** 1st session ***");
            template.process(printAll);
            System.out.println("\n*** 2nd session ***");
            template.process(printAll);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

同じ問い合わせを二回実行しています.
こいつを実行!!

*** 1st session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

*** 2nd session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

ということで,いずれのセッションでも三回ずつ SQL を発行しています.
『…おそっ……』©眞鍋かをり


そんなわけで,キャッシングを有効にしてみましょう.
まずは hibernate.cfg.xml に次の行を追加します.

        <property name="hibernate.cache.provider_class">net.sf.hibernate.cache.HashtableCacheProvider</property>

今回は read-only だし,お手軽そうなので HashTableCacheProvider を使うことにしました.
で,どこからいこうかなぁ? やっぱり内側からということで,モデルのマッピングファイルに,<class> 要素の最初の子として以下の行を追加.

        <cache usage="read-only"/>

そして問い合わせクラスを実行!!

*** 1st session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

*** 2nd session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

ぐはぁっ,何も変わらず.(ToT)


うーみゅ,今の場合,モデルは雑誌のコレクションにアクセスすることで問い合わせされているわけで,ということはコレクションの方にキャッシュの指定をしなきゃいけないのか?
ということで,雑誌のマッピングファイルに,<set> 要素の最初の子として以下の行を追加.

        <cache usage="read-only"/>

そして問い合わせクラスを実行!!

Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

*** 2nd session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
CanCam[Asami Usuda, Yuri Ebihara]
ViVi[Jun Hasegawa, Sayo Aizawa]

やたっ!! \(^o^)/
二つめのセッションで,モデルを問い合わせる SQL が発行されなくなりました.
しかし...


でも、ほんとーーに気持ち程度しか早まっておらず、もどかしさはかわらない。
…………まだ遅いんだよ… ©眞鍋かをり


ということで,雑誌クラス本体もキャッシングを有効にしましょう.
雑誌のマッピングファイルに,<class> 要素の最初の子として以下の行を追加.

        <cache usage="read-only"/>

そして問い合わせクラスを実行!!

Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

*** 2nd session ***
Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
CanCam[Asami Usuda, Yuri Ebihara]
ViVi[Jun Hasegawa, Sayo Aizawa]

ぐはぁっ,何も変わらず.(;_;)
うーみゅ,Session#find() ではキャッシングは働かないのかなぁ?
しょうがないので,問い合わせクラスを次のように修正.

package study;

import java.util.Iterator;

import net.sf.hibernate.Session;

public class Query {
    public static void main(String[] args) {
        try {
            HibernateTemplate template = new HibernateTemplate();

            System.out.println("\n*** 1st session ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    Iterator it = session.find("from study.Magazine")
                            .iterator();
                    while (it.hasNext()) {
                        System.out.println(it.next());
                    }
                    return null;
                }
            });

            System.out.println("\n*** 2nd session ***");
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    for (int i = 0; i < 2; ++i) {
                        System.out.println(session.load(Magazine.class, new Integer(i)));
                    }
                    return null;
                }
            });
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最初のセッションは前と同じですが,二番目のセッションは Session#load() を使っています.
全く実用的ではないと思っちゃいますが...
Hibernate さんに
『最速でお願いします』
と頼んでみた。©眞鍋かをり
ってことで,実行!!!!

Hibernate: select 
               magazine0_.id as id, magazine0_.name as name 
           from 
               Magazine magazine0_
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
Hibernate: select 
               model0_.magazine as magazine__, model0_.id as id__, 
               model0_.id as id0_, model0_.name as name0_ 
           from 
               Model model0_ 
           where 
               model0_.magazine=?
CanCam[Yuri Ebihara, Asami Usuda]
ViVi[Jun Hasegawa, Sayo Aizawa]

*** 2nd session ***
CanCam[Asami Usuda, Yuri Ebihara]
ViVi[Jun Hasegawa, Sayo Aizawa]

やたっ!! \(^o^)/
二番目のセッションで SQL が発行されなくなりました!!!!


最後の所,Session#find()Session#load() に変更したのは,まるっきり役に立つ感じがしませんが,その前のコレクションのキャッシングは効果ありそう.なんたって,N + 1 な問い合わせを回避できたのですから.
今回は read-only な戦略ということで,本当に実用的とは言い難いところですが,例えばバッチでのみ更新されるようなテーブルがあって,バッチ後 Web アプリケーションを再起動するような運用が出来る場合には,使えるかもしれませんね.


ちょっと気になるのは,CacheProvider って,複数の実装を永続クラスごとに切り替えるとか出来ないのかなぁ? ってこと.
今回みたいな read-only な戦略の場合には,HashtableCacheProvider でも十分というか,分散やトランザクションなどを考慮する必要もないのでジャストフィットという気もするわけです.
でも,更新を伴うところではトランザクショナルな CacheProvider が必要になります.
これらを共存させられないのかなぁ? できるとしたら <cache> 要素で指定出来なきゃいけないので,きっと出来ないんだろうなぁ.とすると,read-only な永続クラスでもトランザクショナルな CacheProvider を使うしかないわけで.残念!!!!


それから,実際にキャッシュされるのは永続オブジェクトではなく,そのプロパティ値の配列みたいです.
そんなわけで (どんなわけで?),SQL は発行されなくても,永続オブジェクトのインスタンス化はセッションごとに毎回行われます.


次回は「14.3.3. Strategy: read/write」へ進みます.