Hibernate 入門記 パフォチュー その2 プロキシ

今日は「14.2. Proxies for Lazy Initialization」へ進みます.
そういえば,プロキシ関係はこれまでスキップしまくっていたことを思い出しました.
なんか,問い合せの結果のオブジェクトって,プロキシだったりプロキシじゃなかったりするのですが,どういう場合にプロキシが帰って来るのかさえ知らずにのほほんと過ごしてきたわけで.心より恥じる.
ま,知らないから学習するのですよ.だからいいのだ.
そんなことより,Proxy はプロキシなのかプロクシなのか? プロキシーなのかプロクシーなのか?
ぐぐったらプロキシが断然多いみたいなので,プロキシでいってみよう.
でも,ATOK はプロキシ苦手らしい.ちょっと油断するとすぐに「プロ棋士」と変換しやがる.残念!!!!


そんなわけで (どんなわけで?),プロキシです.
Hibernate は,遅延初期化を実現するために,CGLIB を使って永続オブジェクトのプロキシを作るそうです.
ふむ.つまり,遅延初期化が必要になると,プロキシが作られるわけですか.
それでその,遅延初期化を有効にするには,<class> 要素の proxy 属性で永続クラス自身またはそのインタフェースを指定すればいいようです.
プロキシは,永続クラスのサブクラスだとか.そんなわけで (どんなわけで?),永続クラスは少なくともパッケージスコープを持つデフォルトコンストラクタが必要とのこと.
その他にもプロキシには落とし穴があるそうで...


一つめは,継承に関するもの.
なんかアレなんで,ここからお試しいっちゃいます.後に回すとサボるから (苦笑).
まずはテーブル定義.

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

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

タレントとそのサブクラスであるモデルを table per subclass で.
次にタレントの永続クラス.

package study;

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

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

    public String toString() {
        return name;
    }
}

そしてモデルの永続クラス.

package study;

public class Model extends Talent {
    String magazine;

    public Model() {
    }
    public Model(String name, String magazine) {
        super(name);
        this.magazine = magazine;
    }
    public String toString() {
        return name + " (" + magazine + ")";
    }
}

タレントおよびモデルからなる継承階層のマッピングファイル.まずはプロキシなしで.

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

        <joined-subclass name="Model">
            <key column="id"/>
            <property name="magazine"/>
        </joined-subclass>
    </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 {
                    session.save(new Model("Yuri Ebihara", "CanCam"));
                    session.save(new Model("Sayo Aizawa", "ViVi"));
                    session.save(new Talent("Akiko Yada"));
                    session.save(new Talent("Yukie Nakama"));

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

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

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

public class Query {
    public static void main(String[] args) {
        try {
            HibernateTemplate template = new HibernateTemplate();
            template.process(new HibernateCallback() {
                public Object doProcess(Session session) throws Exception {
                    for (int i = 0; i < 4; ++i) {
                        Object o = session.load(Talent.class, new Integer(i));
                        System.out.println(o + " : " + o.getClass().getName());
                    }
                    return null;
                }
            });
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

こいつを実行!!

Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Yuri Ebihara (CanCam) : study.Model
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Sayo Aizawa (ViVi) : study.Model
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Akiko Yada : study.Talent
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Yukie Nakama : study.Talent

ふむ.プロキシは使われていないようです.
注目は,モデルはちゃんと study.Modelインスタンスが返ってきていることです.


ここで,タレントにプロキシを指定してみましょう.

    <class name="Talent" proxy="Talent">

そして実行!!

net.sf.hibernate.MappingException: All subclasses must also have proxies: study.Talent

ぐはぁっ.(+_+)
サブクラスも一斉にプロキシ指定しないとダメですか.そんな気はしたんですけどね.試してみたかったのだ.
そんなわけで (どんなわけで?),モデルにもプロキシの指定を追加.

        <joined-subclass name="Model" proxy="Model">

そして実行!!

Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Yuri Ebihara (CanCam) : study.Talent$$EnhancerByCGLIB$$64852b61
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Sayo Aizawa (ViVi) : study.Talent$$EnhancerByCGLIB$$64852b61
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Akiko Yada : study.Talent$$EnhancerByCGLIB$$64852b61
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Yukie Nakama : study.Talent$$EnhancerByCGLIB$$64852b61

ふむ.なんか,おもしろい結果ですね.
まず,toString() の結果を見る限り,モデルはちゃんと study.ModeltoString() が呼び出されているようです.名前の後ろに雑誌が表示されていますからね.
しかし,その後ろに表示しているクラス名を見ると,モデルもタレントと同じ,study.Talent$$EnhancerByCGLIB$$64852b61 になっています.


そんなわけで (どんなわけで?),問い合せクラスを次のように変更します.

                    Model model = (Model) session.load(Talent.class,
                        new Integer(0));
                    System.out.println(model + " : "
                        + model.getClass().getName());

ロードしたオブジェクトを,Model にキャストしています.
こいつを実行!!

java.lang.ClassCastException

ぐはぁっ.(+_+)


つまり,プロキシを使うとダウンキャストは出来なくなるということですね.
この場合,Session#load() の第一引数でモデルを指定すればキャストできます.

                    Model model = (Model) session.load(Model.class,
                        new Integer(0));
                    System.out.println(model + " : "
                        + model.getClass().getName());

こいつを実行!!

Hibernate: select 
               model0_.id as id0_, model0_.magazine as magazine1_0_, 
               model0__1_.name as name0_0_ 
           from 
               Model model0_ 
                   inner join Talent model0__1_ on model0_.id=model0__1_.id 
           where 
               model0_.id=?
Yuri Ebihara (CanCam) : study.Model$$EnhancerByCGLIB$$25bbdbe4

ちゃんとモデルのプロキシが返ってきています.


ここまでをまとめると...

  • proxy 属性を指定した永続クラスを Session#load() でロードした場合,第一引数で指定したクラスとしてしか扱うことが出来ず,そのサブクラスへのキャストは出来ない.
  • ただし,サブクラスでオーバーライドしたメソッドは有効 (サブクラスのメソッドが呼び出される).

ちょっと微妙...
ともあれ (JW),これがプロキシ最初の落とし穴.らしい.


落とし穴は他にも.
これまで,一つのセッション中では,DB 中の同じ行を表現する永続オブジェクトは,常に一つと学習してきました.
複数の問い合せで取得した永続オブジェクトでも,同じ行を表現するものなら同一 (==true になる) ということになっていたわけです.
しかし,プロキシを使った場合はそうでもないようで.

                    Talent t = (Talent) session.load(Talent.class, new Integer(0));
                    Model m = (Model) session.load(Model.class, new Integer(0));
                    System.out.println(t == m);

こいつを実行!!

false

ロードするときの型によって,同一の行を表現する複数の永続オブジェクトが返されることが分かります.


ところが!!
このようにキャストできないとか同一じゃないとかっていうのは,あくまでもプロキシの話で,その背後にいる本物の永続オブジェクトは同一のものらしいです.

                    Talent t = (Talent) session.load(Talent.class, new Integer(1));
                    Model m = (Model) session.load(Model.class, new Integer(1));
                    System.out.println(t == m);
                    m.magazine = "BOAO";
                    System.out.println(t);

モデルとして取得した永続オブジェクトを更新した後,タレントとして取得した別個のインスタンスを表示しています.
こいつを実行!!

false
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Sayo Aizawa (BOAO)

なんか,SELECT を実行するタイミングが微妙な感じですが,ともかくモデルで取得した紗世ちゃんを変更すると,タレントで取得した紗世ちゃんの値もちゃんと変わっています.


それから,final なクラスやプロパティの setter/getter が final なメソッドになっているようなクラスはプロキシを作成できないとのこと.


最後にもう一つ.
プロキシは永続クラスのサブクラスなので,永続クラスが何らかのリソース (大量のメモリとか,ファイルハンドルとか) を必要とする場合は,プロキシもそのリソースを必要とするそうです.
プロキシの背後には本物の永続オブジェクトがいる訳なので,リソースは二倍必要になるということですね.


ふーっ,なんかたくさんありましたね...
これらは Java が単一継承しかできないのが原因だそうです.


そんなわけで (どんなわけで?),インタフェースの出番です.インタフェースを使えばキャストの制約も乗り越えられるそうです.
やってみましょう.
まずタレントのインタフェース.

package study;

public interface ITalent {
}

次にモデルのインタフェース.

package study;

public interface IModel extends ITalent {
}

手抜きしていずれもただのマーカー.
タレントとモデルの永続クラスで,それぞれを implements します.
そしてマッピングファイルの proxy 属性でインタフェースを指定するように修正.
問い合せクラスを次のように修正します.

                    IModel m = (IModel) session.load(Talent.class, new Integer(0));
                    System.out.println(m + " : " + m.getClass().getName());

Session#load() の第一引数では,タレントのクラスを指定しています.それをモデルのインタフェースにキャストできるでしょうか?
実行!!

Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Yuri Ebihara (CanCam) : study.ITalent$$EnhancerByCGLIB$$cb4ae33d

おおぉぉぉ... でけた.
さらに,

                    ITalent t = (ITalent) session.load(Talent.class, new Integer(1));
                    IModel m = (IModel) session.load(Model.class, new Integer(1));
                    System.out.println(t == m);

これの結果は

true

同一になった!!
でも,困ったことに...

                    IModel m = (IModel) session.load(Talent.class, new Integer(2));
                    System.out.println(m + " : " + m.getClass().getName());

この結果

Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Akiko Yada : study.ITalent$$EnhancerByCGLIB$$cb4ae33d

モデルじゃないのにキャストできちゃいますから!! 残念!!!!


ものは試しで,IModelgetMagazine() とかつけて,Model でそれを実装して,次のようにやってみました.

                    IModel m = (IModel) session.load(Talent.class, new Integer(2));
                    System.out.println(m + " : " + m.getClass().getName());
                    System.out.println(m.getMagazine());

その結果.

Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
Akiko Yada : study.ITalent$$EnhancerByCGLIB$$cb4ae33d
java.lang.ClassCastException
    at study.IModel$$FastClassByCGLIB$$988a0425.invoke(<generated>)
    at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149)
    at net.sf.hibernate.proxy.CGLIBLazyInitializer.intercept(CGLIBLazyInitializer.java:108)
    at study.ITalent$$EnhancerByCGLIB$$cb4ae33d.getMagazine(<generated>)
    at study.Query$1.doProcess(Query.java:12)
    at study.HibernateTemplate.process(HibernateTemplate.java:39)
    at study.Query.main(Query.java:8)

うーみゅ,m.getMagazine() して初めて ClassCastException ですか... なんかイマイチ.


ともあれ (JW),プロキシを念頭に置くなら永続クラスでもインタフェースを用意する方がよさげってことかも.
本当によさげなのか疑問の余地ありですが.


もうちょっと続きます.
次の操作は,プロキシの初期化をすることなく呼び出せるとのことです.

equals()
ただしオーバーライドしていない場合.
hashCode()
ただしオーバーライドしていない場合.
identifier の getter
でも identifier って? ID プロパティだと初期化しようとするから違うものかも?

オーバーライドしていない場合っていうのは,Objectequals()/hashCode() そのままの場合ってことでしょう.
でもでも,永続クラスは ID プロパティの同値性をチェックするように equals() をオーバーライドすべきではなかったか?
equals() をオーバーライドしたら,hashCode() もオーバーライドすべきな訳で,あまり意味がないような?
ともあれ (JW),Hibernate は永続クラスが equals()/hashCode() をオーバーライドしているかどうかを検出するそうです.検出してどうするかは... 書いてないよ? まいっか.


さてさて,ここまで書いてきてなんですが,プロキシの初期化っていったいどういうことよ? 何してくれるのさ?
なんか,ちゃんとした解説が見あたらないのですが,ここまでいろいろ試してきてちょっとだけ分かったことがあります.

  • プロキシは実体 (DB の行) がなくても返ってくる!!


これは,たとえば次のように試した場合.

                    System.out.println("before load");
                    Object o = session.load(Talent.class, new Integer(5));
                    System.out.println("after load");
                    System.out.println(o + " : " + o.getClass().getName());

今のデータには,ID が 5 の行はありません.4もないんだけど.
その結果.

before load
after load
Hibernate: select 
               talent0_.id as id0_,  casewhen(talent0__1_.id is not null, 1,  
               casewhen(talent0_.id is not null, 0, -1)) as clazz_0_, 
               talent0_.name as name0_0_, talent0__1_.magazine as magazine1_0_ 
           from 
               Talent talent0_ 
                   left outer join Model talent0__1_ on talent0_.id=talent0__1_.id 
           where 
               talent0_.id=?
- Exception initializing proxy
net.sf.hibernate.ObjectNotFoundException: No row with the given identifier exists: 5, of class: study.Talent
    at net.sf.hibernate.ObjectNotFoundException.throwIfNull(ObjectNotFoundException.java:24)
    at net.sf.hibernate.impl.SessionImpl.immediateLoad(SessionImpl.java:1936)
    at net.sf.hibernate.proxy.LazyInitializer.initialize(LazyInitializer.java:53)
    at net.sf.hibernate.proxy.LazyInitializer.initializeWrapExceptions(LazyInitializer.java:60)
    at net.sf.hibernate.proxy.LazyInitializer.getImplementation(LazyInitializer.java:164)
    at net.sf.hibernate.proxy.CGLIBLazyInitializer.intercept(CGLIBLazyInitializer.java:108)
    at study.ITalent$$EnhancerByCGLIB$$9584830b.toString(<generated>)
    at java.lang.String.valueOf(String.java:2131)
    at java.lang.StringBuffer.append(StringBuffer.java:370)
    at study.Query$1.doProcess(Query.java:13)
    at study.HibernateTemplate.process(HibernateTemplate.java:39)
    at study.Query.main(Query.java:8)

DB に初めて問い合せに行くのが,Session#load() から戻ってきた後,toString() を呼び出したときなんですね.
それまで,その永続オブジェクト (もどき?) が本当に実在するものかどうか分からないわけです.
...
うれしいか?


おそらく,これは Session#load() などで単独のインスタンスを引っ張ってくる場合にはあまりうれしくなさそう.
やっぱり,関連に使われる場合に効果があるのではないかと.
その場合,関連元の永続オブジェクトをロードした際に,関連先の ID は外部キーとして取得することが出来ますが,実際に関連先のテーブルをアクセスするのは,もったいないかもしれない.アクセスするかどうか分からないから.
そんな場合にプロキシを置いておけば,アプリがプロキシに触った瞬間,関連先のテーブルをフェッチすればいいわけで.
そういうことかなぁ?


最後に.
プロキシが初期化済みかチェックするには,Hibernate (というクラス) の static メソッド

    • boolean isInitialized(Object proxy)

を使うことができます.
強制的に初期化する (DB からフェッチする) には,同じく Hibernatestatic メソッド

    • void initialize(Object proxy)

を使うことが出来ます.


ということで,「14.2. Proxies for Lazy Initialization」終了っぽいんですが,なんかパフォチューって気がしないんですけどぉ.
パフォチュらせてくださぁぁぁぁぁい!!!!!


でも大丈夫です.次回は「14.3. The Second Level Cache」なのです.
これですよ,私が待ち望んでいたパフォチューねたは.
しかし,明日は EbiYuri デーなので,次回は来週ですから!! 残念!!!!
EbiYuri 斬り!!


よし,02:02 に間に合ったな.
なんとなく,きりのいい時間とか並びのいい時間に更新したい年頃なのです.
今日 (もう昨日だけど) は 09/09 だったので,09:09 に更新したくて,朝から紗世ちゃんのレポートしたわけですよ.へへっ © えみちい
おかげで雨宿りする羽目になって 00:00 には更新できなかったけど,その後 01:01 ,01:10 ときて,02:02 にこれをアップすれば完璧!!
最後はベストセレクションを 03:03 かな♪