第19章 パフォーマンスの改善

19.1. フェッチ戦略

フェッチ戦略 は、アプリケーションが関連をナビゲートする必要があるときに、Hibernate が関連オブジェクトを復元するために使用する戦略です。フェッチ戦略は O/R マッピングのメタデータに宣言するか、特定の HQL 、 Criteria クエリでオーバーライドできます。
Hibernate3 は次に示すフェッチ戦略を定義しています:
  • 結合フェッチ:Hibernate は OUTER JOIN を使って、関連するインスタンスやコレクションを同じSELECT 内で獲得します。
  • セレクトフェッチ:2回目の SELECT で関連するエンティティやコレクションを獲得します。 lazy="false" で明示的に遅延フェッチを無効にしなければ、この2回目の select は関連にアクセスしたときのみ実行されます。
  • サブセレクトフェッチ:2回目の SELECT で、直前のクエリやフェッチで復元したすべての要素に関連するコレクションを復元します。lazy="false" を指定し、明示的に遅延フェッチを無効にしなければ、この2回目の select は関連にアクセスしたときのみ実行されます。
  • バッチフェッチ:セレクトフェッチのための最適化された戦略 - Hibernate は、主キーや外部キーのリストを指定することによりエンティティのインスタンスやコレクションの一群を1回の SELECT で獲得します。
Hibernate は次に示す戦略とも区別をします:
  • 即時フェッチ:所有者のオブジェクトがロードされたときに、関連、コレクション、あるいは属性は即時にフェッチされます。
  • 遅延コレクションフェッチ:アプリケーションがコレクションに対して操作を行ったときにコレクションをフェッチします。これはコレクションでデフォルトとなっています。
  • 「Extra-lazy」コレクションフェッチ:コレクションの個別要素は随時データベースから取得されます。Hibernate は必要でなければ、コレクション全体をメモリにフェッチしないようにします。これは大規模なコレクションに敵しています。
  • プロキシフェッチ:単一値関連は、識別子の getter 以外のメソッドが関連オブジェクトで呼び出されるときにフェッチされます。
  • 「プロキシなし」フェッチ:単一値関連は、インスタンス変数にアクセスがあるとフェッチされます。プロキシフェッチと比較すると、この方法は遅延の度合いが少なく、識別子のみにアクセスがあっても、この間連はフェッチされます。また、このアプリケーションにはプロキシを見せないため、より透過的となっています。この方法はビルド時のバイトコード組み込みが必要になり、使う場面はまれです。
  • 遅延属性フェッチ:属性や単一値関連は、インスタンス変数にアクセスがあればフェッチされます。この方法はビルド時のバイトコード組み込みが必要になり、使う場面はまれです。
ここでは、直交概念が2つあります: いつ 関連をフェッチするか、そして、 どのように フェッチするか。重要なのは、これらを混同しないことです。fetch はパフォーマンスチューニングに使い、lazy はあるクラスの分離されたインスタンスのうち、どのデータを常に使用可能にするかの取り決めを定義するのに利用可能です。

19.1.1. 遅延関連の使いかた

デフォルトでは、Hibernate3 はコレクションに対し遅延セレクトフェッチを使い、単一値関連には遅延プロキシフェッチを使います。このようなデフォルトとなっているのは、 大半のアプリケーションにある関連の多くで、つじつまがあいます。
hibernate.default_batch_fetch_size をセットすると、Hibernate は遅延フェッチにバッチフェッチの最適化を利用します。この最適化はより細かいレベルで実現できます。
Hibernate の session がオープンでないコンテキストで遅延関連にアクセスすると、例外が発生することに注意してください。例:
s = sessions.openSession();
Transaction tx = s.beginTransaction();
            
User u = (User) s.createQuery("from User u where u.name=:userName")
    .setString("userName", userName).uniqueResult();
Map permissions = u.getPermissions();

tx.commit();
s.close();

Integer accessLevel = (Integer) permissions.get("accounts");  // Error!
Session がクローズされたとき、permissions コレクションは初期化されていないため、このコレクションは自身の状態をロードできません。Hibernate は切り離されたオブジェクトの遅延初期化はサポートしていません。修正方法として、コレクションから読み込むコードをトランザクションをコミットする直前に移動させます。
他には、lazy="false" を関連マッピングに指定することで、遅延処理をしないコレクションや関連を使うことが出来ます。しかしながら、遅延初期化はほぼすべてのコレクションや関連で使われることを意図しています。オブジェクトモデル内に遅延処理なしの関連を多く定義してしまうと、 Hibernate は、トランザクション毎にデータベース全体をメモリにフェッチすることになるでしょう。
一方、特定のトランザクションにおいてセレクトフェッチの代わりに結合フェッチ(基本的にこれはnon-lazy)を選択することができます。これからフェッチ戦略をカスタマイズする方法をお見せします。Hibernate3 では、単一値関連とコレクションにおいてフェッチ戦略を選択する仕組みは、全く同じです。

19.1.2. フェッチ戦略のチューニング

セレクトフェッチ(デフォルト)は N+1 セレクト問題という大きな弱点があるため、マッピング定義で結合フェッチを有効にすることができます:
<set name="permissions" 
            fetch="join">
    <key column="userId"/>
    <one-to-many class="Permission"/>
</set
<many-to-one name="mother" class="Cat" fetch="join"/>
マッピング定義で定義した フェッチ 戦略は次のものに影響します:
  • get()load() による復元
  • 関連にナビゲートしたときに発生する暗黙的な復元
  • Criteria クエリ
  • サブセレクト フェッチを使う HQL クエリ
どのフェッチ戦略を使ったとしても、遅延ではない定義済みグラフはメモリに読み込まれることが保証されます。しかし、つまり、特定の HQL クエリを実行するためにいくつかの SELECT 文が即時実行されることがあります。
通常は、マッピングドキュメントでフェッチのカスタマイズは行いません。代わりに、デフォルトの動作を保ち、HQL で left join fetch を指定することで特定のトランザクションで動作をオーバーライドします。これは Hibernate に初回のセレクトで外部結合を使って関連を先にフェッチするように指定しています。Criteria クエリの API では、 setFetchMode(FetchMode.JOIN) を使うことが出来ます。
get()load() で使われるフェッチ戦略を変えたいと感じたときには、単純に Criteria クエリを使うことができます。例:
User user = (User) session.createCriteria(User.class)
                .setFetchMode("permissions", FetchMode.JOIN)
                .add( Restrictions.idEq(userId) )
                .uniqueResult();
これはいくつかの ORM ソリューションが「fetch plan」と呼んでいるものと同じです。
N+1 セレクト問題へのまったく違うアプローチは、2次キャッシュを使うことです。

19.1.3. 単一端関連プロキシ

コレクションの遅延フェッチは、Hibernate 自身の実装による永続コレクションを使って実現しています。しかし、単一端関連における遅延処理では、違う仕組みが必要です。対象の関連エンティティはプロキシでなければなりません。Hibernate はCGLIB ライブラリを介し、実行時のバイトコード拡張を使って永続オブジェクトの遅延初期化プロキシを実現しています。
デフォルトでは、Hibernate3 はすべての永続クラスのプロキシを生成し、それらを使って、many-to-oneone-to-one 関連の遅延フェッチを可能にしています。
マッピングファイルで proxy 属性によって、クラスのプロキシインターフェースとして使うインターフェースを宣言できます。デフォルトでは、Hibernate はそのクラスのサブクラスを使います。プロキシクラスは少なくともパッケージ可視でデフォルトコンストラクタを実装する必要があります。すべての永続クラスにこのコンストラクタを推奨します。
ポリモーフィズムのクラスに対してもこの方法を適用する場合、いくつか問題が発生する可能性があります。例:
<class name="Cat" proxy="Cat">
    ......
    <subclass name="DomesticCat">
        .....
    </subclass>
</class>
第一に、 Cat のインスタンスは DomesticCat にキャストできません。たとえ基となるインスタンスが DomesticCat であったとしてもです:
Cat cat = (Cat) session.load(Cat.class, id);  // instantiate a proxy (does not hit the db)
if ( cat.isDomesticCat() ) {                  // hit the db to initialize the proxy
    DomesticCat dc = (DomesticCat) cat;       // Error!
    ....
}
第二に、プロキシの == は成立しないことがあります:
Cat cat = (Cat) session.load(Cat.class, id);            // instantiate a Cat proxy
DomesticCat dc = 
        (DomesticCat) session.load(DomesticCat.class, id);  // acquire new DomesticCat proxy!
System.out.println(cat==dc);                            // false
しかし、これは見かけほど悪い状況というわけではありません。たとえ異なったプロキシオブジェクトへの二つの参照があったとしても、基となるインスタンスは同じオブジェクトです:
cat.setWeight(11.0);  // hit the db to initialize the proxy
System.out.println( dc.getWeight() );  // 11.0
第三に、 final クラスや final メソッドを持つクラスに CGLIB プロキシを使えません。
最後に、永続オブジェクトのインスタンス化時 (例えば、初期化子やデフォルトコンストラクタの中で) になんらかのリソースを取得するなら、そのリソースもまたプロキシを通して取得されます。実際には、プロキシクラスは永続クラスにある実際のサブクラスです。
これらの問題は Java の単一継承モデルにある原理上の制限が原因となっています。これらの問題を避けたいのなら、ビジネスメソッドを宣言したインターフェースをそれぞれ永続クラスで実装しなければなりません。CatImpl がインターフェース DomesticCat 実装のCatDomesticCatImplと言うインターフェースを実装するマッピングファイルでこれらのインターフェースを指定する必要があります。例:
<class name="CatImpl" proxy="Cat">
    ......
    <subclass name="DomesticCatImpl" proxy="DomesticCat">
        .....
    </subclass>
</class>
そうすると、load() あるいは iterate()CatDomesticCat のインスタンスのプロキシを返すことができます。
Cat cat = (Cat) session.load(CatImpl.class, catid);
Iterator iter = session.createQuery("from CatImpl as cat where cat.name='fritz'").iterate();
Cat fritz = (Cat) iter.next();

注記

list() は通常、プロキシを返しません。
関連も遅延初期化されます。これはプロパティを Cat 型で宣言しなければならないことを意味します。 CatImpl ではありません。
プロキシの初期化を 必要としない 操作も存在します:
  • equals():永続クラスが equals() をオーバーライドしないとき
  • hashCode():永続クラスが hashCode() をオーバーライドしないとき
  • 識別子の getter メソッド
Hibernate は equals()hashCode() をオーバーライドした永続クラスを検出します。
デフォルトの lazy="proxy" の代わりに、 lazy="no-proxy" を選ぶと、型変換(キャスト)に関連する問題を回避することが出来ます。しかし、ビルド時のバイトコード組み込みが必要になり、どのような操作であっても、ただちにプロキシの初期化を行われます。

19.1.4. コレクションとプロキシの初期化

LazyInitializationException は、Session のスコープ外から初期化していないコレクションやプロキシにアクセスがあると、Hibernate によってスローされます。すなわち、コレクションやプロキシへの参照を持つエンティティが分離された状態の時です。
Session をクローズする前にプロキシやコレクションの初期化を確実に行いたいときがあります。もちろん、cat.getSex()cat.getKittens().size() などを常に呼び出すことで初期化を強制することはできます。しかしこれはコードを読む人を混乱させ、汎用的なコードという点からも不便です。
static メソッドの Hibernate.initialize()Hibernate.isInitialized() は遅延初期化のコレクションやプロキシを扱うときに便利な方法をアプリケーションに提供します。 Hibernate.initialize(cat) は、Session がオープンしている限りは cat プロキシを強制的に初期化します。Hibernate.initialize( cat.getKittens() ) は kittens コレクションに対して同様の効果があります。
別の選択肢として、必要なすべてのコレクションやプロキシがロードされるまで Session をオープンにしておく方法があります。アプリケーションのアーキテクチャによって、特に Hibernate によるデータアクセスを行うコードと、それを使うコードが異なるアプリケーションのレイヤーや、物理的に異なるプロセスにある場合、コレクション初期化時に Session を確実にオープンに保つ部分で問題があります。この問題の対応には2つの基本的な方法があります:
  • Web ベースのアプリケーションでは、ビューのレンダリングが完了し、ユーザーリクエストの最後でのみ、サーブレットフィルタを利用し Session をクローズすることができます(Open Session in View パターンです)。もちろん、アプリケーション基盤の例外処理の正確性が非常に重要になります。ビューのレンダリング中に例外が発生したときでさえ、ユーザーに処理が戻る前に Session のクローズとトランザクションの終了を行うことが不可欠になります。 Hibernate の Wiki に載っている 「Open Session in View」 パターンの例を参照してください。
  • ビジネス層が分離しているアプリケーションでは、ビジネスロジックは Web 層で必要になるすべてのコレクションを(値を)返す前に「準備」する必要があります。つまり、これは特定のユースケースで必要となるプレゼンテーション/ Web 層に対し、ビジネス層がすべてのデータをロードし、すべてのデータを初期化して返すべきということです。通常、アプリケーションは Web 層で必要な各コレクションに対して Hibernate.initialize() を呼び出すか(この呼び出しはセッションをクローズする前に行う必要があります)、 Hibernate クエリの FETCH 節や CriteriaFetchMode.JOIN を使ってコレクションを先に取得します。普通は Session Facade の代わりに Command パターンを採用するほうがより簡単です。
  • 初期化されていないコレクション、もしくは他のプロキシにアクセスする前に、 merge()lock() を使って新しい Session に以前にロードされたオブジェクトを追加することも出来ます。臨時トランザクションのセマンティクスを導入したので、 Hibernate はこれを自動的に行わず、行うべきでもありません
大きなコレクションを初期化したくはないが、コレクションについてのなんらかの情報(サイズのような)やデータのサブセットを必要とすることがあります。
コレクションフィルタを使うことで、初期化せずにコレクションのサイズを取得することが出来ます:
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
createFilter() メソッドは、コレクション全体を初期化する必要なしに、コレクションのサブセットを復元するために効果的に使えます:
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();

19.1.5. バッチフェッチの使用

バッチフェッチを利用し、Hibernate は一つのプロキシにアクセスがあると、Hibernate は初期化していない複数のプロキシをロードすることができます。バッチフェッチは遅延セレクトフェッチ戦略に対する最適化です。バッチフェッチの調整には2つの方法があります。クラスレベルとコレクションレベルです。
クラス、要素のバッチフェッチは理解が比較的簡単です。実行時の次の場面を想像してください。Session にロードされた25個の Cat インスタンスが存在し、各 Catowner である Person への関連を持ちます。Person クラスは lazy="true" のプロキシでマッピングされています。今すべての Cat に対して繰り返し getOwner() を呼び出すと、Hibernate はデフォルトでは25回の SELECT を実行し、owner プロキシの取得をします。この動作を Person のマッピングの batch-size の指定で調整できます。
<class name="Person" batch-size="10">...</class>
Hibernate はクエリを3回だけを実行するようになります:パターンは 10, 10, 5 です。
コレクションのバッチフェッチも有効にすることが出来ます。例として、それぞれの PersonCat の遅延コレクションを持っており、 10 個の Person が Sesssion にロードされたとすると、すべての Person に対して繰り返し getCats() を呼び出すことで、計10回の SELECT が発生します。Person のマッピングで cats コレクションのバッチフェッチを有効にすれば、 Hibernate はコレクションの事前フェッチが出来ます。
<class name="Person">
    <set name="cats" batch-size="3">
        ...
    </set>
</class>
batch-size が 3 なので、 Hibernate は 4 回の SELECT で 3 個、3 個、3 個、1 個をロードします。繰り返すと、属性の値は特定の Session の中の初期化されていないコレクションの期待数に依存します。
コレクションのバッチフェッチはアイテムのネストしたツリー、すなわち、代表的な部品表のパターンがある場合に特に有用です。しかし、読み込みが多いツリーでは ネストした set具体化したパス のほうが選択肢としては良いでしょう。

19.1.6. サブセレクトフェッチの使用

一つの遅延コレクションや単一値プロキシがフェッチされなければならない場合、 Hibernate はそれらすべてをロードし、サブセレクトのオリジナルクエリが再度実行されます。これはバッチフェッチと同じ方法で動き、逐次ロードはありません。

19.1.7. 遅延プロパティフェッチの使用

Hibernate3 はプロパティごとの遅延フェッチをサポートしています。この最適化手法は グループのフェッチ としても知られています。これは多くの場合マーケティング機能であることに注意してください。実際には列読み込みの最適化よりも、行読み込みの最適化が非常に重要です。しかし、クラスのプロパティの一部だけを読み込むことは極端な事例において便利です。たとえば、レガシーテーブルが何百ものカラムを持ち、データモデルを改善できないなどです。
遅延プロパティ読み込みを有効にするには、対象のプロパティのマッピングで lazy 属性をセットしてください:
<class name="Document">
       <id name="id">
        <generator class="native"/>
    </id>
    <property name="name" not-null="true" length="50"/>
    <property name="summary" not-null="true" length="200" lazy="true"/>
    <property name="text" not-null="true" length="2000" lazy="true"/>
</class>
遅延プロパティ読み込みはビルド時のバイトコード組み込みを必要とします。永続クラスが拡張されていない場合、Hibernate は遅延プロパティの設定を無視して、即時フェッチに戻します。
バイトコード組み込みは以下の Ant タスクを使ってください:
<target name="instrument" depends="compile">
    <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask">
        <classpath path="${jar.path}"/>
        <classpath path="${classes.dir}"/>
        <classpath refid="lib.class.path"/>
    </taskdef>

    <instrument verbose="true">
        <fileset dir="${testclasses.dir}/org/hibernate/auction/model">
            <include name="*.class"/>
        </fileset>
    </instrument>
</target>
不要な列を読み込まないための別の方法は、少なくとも読み込みのみのトランザクションにおいては、HQL や Criteria クエリの射影機能を使うことです。この方法はビルド時のバイトコード組み込みが不要になり、確実のこちらのほうが推奨される解決方法です。
HQL で fetch all properties を使うことで、普通どおりのプロパティの即時フェッチングを強制することが出来ます。