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

14.1. コレクションのパフォーマンスの理解

すでにコレクションの話題に結構な時間を割きました。この節ではコレクションが実行時にどのように振る舞うかについて話題を2,3取り上げます。

14.1.1. 分類

Hibernateは以下の3つの種類のコレクションを定義しています。

  • 値のコレクション

  • one-to-many関連

  • many-to-many関連

この分類法ではさまざまなテーブルと外部キー関係を分類しました。しかしこれは関係モデルについて知る必要のあることについては、ほとんど何も教えてはくれません。関係構造とパフォーマンスの特徴を完全に理解するためには、コレクションの行を更新、削除するためにHibernateが使用する主キーの構造を考えなくてはなりません。このことは以下の分類を示唆しています。

  • インデックス付きのコレクション

  • set

  • bag

インデックス付きのコレクション(map、list、配列)はすべて、<key><index>カラムからなる主キーがあります。この場合コレクションの更新は通常、非常に効率的です。主キーは効率的にインデックスを付けられ、Hibernateが更新または削除しようとするとき、特定の行は効率的に検索されます。

setは<key>で構成される主キーと要素のカラムを持っています。これはコレクション要素の型のいくつかについては効率的ではないかもしれません。特に複合要素、ラージ・テキスト、バイナリ・フィールドなどは非効率です。それはデータベースが複合主キーを効率的に索引付けできないことがあるからです。一方で、one-to-many関連やmany-to-many関連、特に人工識別子の場合は同じくらい効率的です。(余談:SchemaExport<set>の主キーを実際に生成させたいなら、すべてのカラムをnot-null="true"と定義しなければなりません。)

bagは最悪のケースです。bagは重複した要素の値を許し、インデックスカラムを持たないので、主キーは定義されません。Hibernateには重複した行同士を区別する方法がありません。Hibernateはこの問題を、変更が行なわれたときには常に完全にコレクションを削除し(一度のDELETEで)、もう一度作成しなおすことで解決します。 これは非常に非効率です。

one-to-many関連にとって、「主キー」がデータベーステーブルの物理的な主キーではないかもしれないことに注意してください。しかしこのケースでは以上の分類はまだ有用です。(この分類は、まだHibernateがどのようにコレクションの個別の行を「検索」しているかを反映しています。)

14.1.2. コレクションの更新に最も効率的なlist、map、set

上での議論から、インデックス付きのコレクションと(大抵は)setが要素の追加、削除、更新などの操作を最も効率的に行うことは明らかです。

まず間違いなく、many-to-many関連や値のコレクションにおいて、インデックス付きのコレクションのSetに対するアドバンテージが1つ以上あります。Setはその構造のため、要素が「変更」されたときHibernateは決して行をUPDATEしません。setへの変更は必ず(個別の行に対する)INSERTDELETEを通して行われます。繰り返しになりますが、これはone-to-many関連には当てはまりません。

配列がlazyではいけないという決まりなので、list、map、setが最も効率の良いコレクション型であると結論付けるでしょう(setはいくつかの値のコレクションに対しては効率的ではありませんが)。

Hibernateのアプリケーションで、setは最も一般的な種類のコレクションであると思われます。

今回のリリースではドキュメント化されていない特徴があります。 <idbag>を使ったマッピングは値のコレクションやmany-to-many関連のためのbagセマンティクスの実装であり、このケースにおいては他のどのスタイルのコレクションよりも効率的です!

14.1.3. インバース・コレクションに最も効率的なbagとlist

bagを見放してしまう前に、bag(そしてlist)がsetよりずっと効率的である特別なケースをご紹介します。inverse="true"であるコレクションに対して(例えば、普通のone-to-many双方向関連)、初期化(フェッチ)の 必要なしにbagまたはlistに要素を追加することができます!これはCollection.add()Collection.addAll()Setとは違って、bagまたはListには必ずtrueを返さなければならない からです。これにより、以下のようなよくあるコードが非常に高速になります。

Parent p = (Parent) sess.load(Parent.class, id);
    Child c = new Child();
    c.setParent(p);
    p.getChildren().add(c);  //コレクションをフェッチする必要はありません!
    sess.flush();

14.1.4. 一括削除

コレクションの要素を一つ一つ削除するのが極めて非効率であることがあります。Hibernateは全くの愚か者というわけではありませんから、新しい空のコレクションのケース(例えばlist.clear()をコールするケース)では一つ一つの要素を削除すべきでないとわかっています。このケースでは、HibernateはDELETEを一度だけ発行し、それですべて済みます!

サイズが20のコレクションに1つ要素を追加し、そして2つ要素を削除するとします。HibernateはINSERTステートメントを1つとDELETEステートメントを2つ発行します(もしコレクションがbagでなければ)。これは確かに望ましい動作です。

しかし、2つの要素を残して18個を削除し、それから新しい要素を3つ追加するとします。この場合2つの方法がありえます。

  • 1ずつ18個の行を削除し、そして3つの行を挿入する。

  • コレクション全体を削除し(一つのSQL DELETEで)、そして5つの要素をすべて(一つ一つ)挿入する。

このケースにおいておそらく2番目の方法の方が高速だろうとわかるほど、Hibernateは賢くありません(そして恐らく、Hibernateがそのように賢いのは望ましいことではないでしょう。例えば、そのような振る舞いでデータベーストリガが混乱してしまうなどということがあるかもしれません)。

幸い元のコレクションを捨て(つまり参照をやめる)、現在の要素すべて持つような新しくインスタンス化したコレクションを返すことで、いつでもこの振る舞い(つまり2番目の戦略)を強制することができます。時にこれは非常に有用で強力な方法となることがあります。

コレクションのマッピングについての章で、永続性コレクションに対してどのようにlazy初期化を使用できるか説明しました。CGLIBを使用することで、通常のオブジェクト参照に対しても同様の効果を得ることが出来ます。また、SessionレベルでHibernateがどのように永続オブジェクトをキャッシュするかについても言及しました。より積極的なキャッシュ戦略はclass-by-class戦略に基づいた設定が可能です

次の節で、必要に応じてより高いパフォーマンスを達成するための機能の使い方について説明します。

14.2. lazy初期化のためのプロキシ

Hibernateは実行時バイトコードエンハンスメントを使い、永続オブジェクトに対してlazy初期化プロキシを実装しています(CGLIBという素晴らしいライブラリを使用しています)。

マッピング定義ファイルで、そのクラスのプロキシ・インタフェースとしてクラスやインタフェースを定義します。以下のように、クラス自身を指定するのがお勧めの方法です:

<class name="eg.Order" proxy="eg.Order">

プロキシの実行時の型はOrderのサブクラスになります。プロキシとして指定するクラスは、少なくともパッケージレベルの可視性のデフォルト・コンストラクタを実装しなければならないことに注意してください。

このアプローチをポリモーフィックなクラスに拡張するとき気をつけるべきことがあります。

<class name="eg.Cat" proxy="eg.Cat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.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() ) {                  // プロキシを初期化するためにdbをヒットします
    DomesticCat dc = (DomesticCat) cat;       // エラー!
    ....
}

2番目に、 == が成立しない可能性があります。

Cat cat = (Cat) session.load(Cat.class, id);            // Catプロキシの初期化
DomesticCat dc = 
    (DomesticCat) session.load(DomesticCat.class, id);  // 新しいDomesticCatを要求!
System.out.println(cat==dc);                            // false

しかし、これは見かけほど悪い状況というわけではありません。以下のように、たとえ異なったプロキシ・オブジェクトに対して二つの参照があったとしても、基となるインスタンスは同じオブジェクトです:

cat.setWeight(11.0);  // プロキシを初期化するためにdbをヒットします
System.out.println( dc.getWeight() );  // 11.0

3番目に、finalクラスやfinalメソッドを持つクラスにCGLIBプロキシを使えません。

最後に、もし永続オブジェクトにインスタンス化時にリソースが必要となるなら(例えば、イニシャライザやデフォル・トコンストラクタ内で)、そのリソースもまたプロキシを通して取得されます。実際は、そのプロキシクラスは永続クラスのサブクラスです。

これらの問題はJavaの単一継承モデルの原理上の制限のためです。もしこれらの問題を避けたいのなら、永続クラスひとつひとつに、ビジネス・メソッドを定義したインターフェイスを実装させなければなりません。そしてこれらのインターフェイスをマッピング定義ファイルでプロキシとして指定します。:

<class name="eg.Cat" proxy="eg.ICat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.IDomesticCat">
        .....
    </subclass>
</class>

CatはインターフェイスICatを、DomesticCatはインターフェイスIDomesticCatを実装します。その際、CatDomesticCatのインスタンスはload()iterate()によって返されます(find()はプロキシを返さないことに注意してください)。

ICat cat = (ICat) session.load(Cat.class, catid);
Iterator iter = session.iterate("from cat in class eg.Cat where cat.name='fritz'");
ICat fritz = (ICat) iter.next();

関係もまたlazyに初期化されます。これはCatではなく ICat型でプロパティを定義しなければならないということです。

以下のようなプロキシの初期化を必要としない操作も存在します。

  • 永続クラスがequals()をオーバーライドしないときのequals()

  • 永続クラスがhashCode()をオーバーライドしないときのhashCode()

  • 識別子のgetterメソッド

Hibernateはequals()hashCode()をオーバーライドする永続クラスを検知します。

プロキシを初期化するときに発生する例外はLazyInitializationExceptionでラップされます

Sessionをクローズする前に、確実にプロキシやコレクションを初期化しなければならないときがあります。もちろん、例えばcat.getSex()cat.getKittens().size()をコールして、強制的に初期化することは常に可能です。しかしこれはソースを読む人を混乱させますし、汎用的なコードの観点からも不便です。staticメソッドであるHibernate.initialize()Hibernate.isInitialized()を使うと、アプリケーションでlazyに初期化されたコレクションやプロキシを便利に使用できます。Hibernate.initialize(cat)Sessionがオープンである限り、catプロキシを強制的に初期化します。Hibernate.initialize( cat.getKittens() )はkittenコレクションに対して同様の効果を持ちます。

14.3. バッチ・フェッチングの使用

Hibernateではバッチ・フェッチングを効率的に使用できます。すなわち、1つのプロキシがアクセスされる場合、Hibernateはいくつかの初期化されていないプロキシをロードすることができます。バッチ・フェッチングはレイジー・ローディング戦略に対する最適化です。バッチ・フェッチングの調整にはクラス・レベルとコレクション・レベルの2つの方法があります。

クラス/エンティティに対するバッチ・フェッチングは理解しやすいです。実行時に以下の状況にあると創造してください。:Sessionに25のCatインスタンスがロードされていて、それぞれのCatPerson型であるownerへの参照を持っています。Personクラスはプロキシによりマッピングされており、lazy="true"です。もしすべてのcatsをイテレートしそれぞれgetOwner()をコールするならば、Hibernateはデフォルトではプロキシのownersを読み出すために、25のSELECT文を実行します。Personのマッピング定義のbatch-sizeを指定することで、この振る舞いを調整することができます。:

<class name="Person" lazy="true" batch-size="10">...</class>

Hibernateはパターンが10つ,10つ,5つであるクエリを3つだけ実行します。パフォーマンスの最適化に関する限り、バッチ・フェッチングは当て推量であり、それは特定のSession中での初期化されていないプロキシの数に依存します。

コレクションのバッチ・フェッチングも可能です。例えば各PersonCatのlazyコレクションを持ち、現在10のPersonインスタンスがSessionにロードされています。すべてのPersonインスタンスをイテレートすることで、すべてのgetCats()コールにつき1つ、計10のSELECTが発生します。もしPersonのマッピング定義でcatsコレクションに対してバッチ・フェッチングの指定が可能ならば、Hibernateはコレクションのプリ・フェッチが可能です。

<class name="Person">
    <set name="cats" lazy="true" batch-size="3">
        ...
    </set>
</class>

batch-sizeが3なので、Hibernateは4回のSELECT文で3つ,3つ,3つ,1つのコレクションをそれぞれロードします。属性の値は特定のSession中で期待される初期化されていないコレクションの数に依存します。

コレクションのバッチ・フェッチングは、アイテムのネストツリーの場合、つまり典型的なbill-of-materialsパターンの場合に有用です。

14.4. 第2レベル・キャッシュ

HibernateのSessionは永続データのトランザクションレベルのキャッシュです。class-by-classとcollection-by-collectionごとの、クラスタレベルやJVMレベル(SessionFactoryレベル)のキャッシュを設定することも可能です。クラスタ化されたキャッシュにつなぐことさえ可能です。キャッシュは他のアプリケーションによって永続ストアに加えられた変更は考慮しないことに注意してください(キャッシュデータを定期的に期限切れに設定することはできます)。

デフォルトでは、HibernateはJVMレベルキャッシュにEHCacheを使います(JCSサポートは現在推奨されておらず、Hibernateの将来のバージョンで削除されます)。hibernate.cache.provider_classプロパティを使い、net.sf.hibernate.cache.CacheProviderを実装したクラスの名前を指定することで他の実装を選ぶことができます。

表 14.1. キャッシュ・プロバイダ

キャッシュプロバイダクラスタイプクラスタセーフクエリキャッシュサポート
Hashtable(製品用として意図されていません)net.sf.hibernate.cache.HashtableCacheProviderメモリ yes
EHCachenet.sf.hibernate.cache.EhCacheProviderメモリ、ディスク yes
OSCachenet.sf.hibernate.cache.OSCacheProviderメモリ、ディスク yes
SwarmCachenet.sf.hibernate.cache.SwarmCacheProviderクラスタ(IPマルチキャスト)yes (clustered invalidation) 
JBoss TreeCachenet.sf.hibernate.cache.TreeCacheProviderクラスタ(IPマルチキャスト), トランザクションyes (複製)yes (clock sync req.)

14.4.1. キャッシュのマッピング

クラスまたはコレクションのマッピングの<cache>要素は以下の形式です:

<cache 
    usage="transactional|read-write|nonstrict-read-write|read-only"  (1)
/>
(1)

usage はキャッシュ戦略を指定します: transactional, read-write, nonstrict-read-write または read-onlyです

または(より良い方法として?)、<class-cache><collection-cache>要素をhibernate.cfg.xmlの中で指定することができます。

usage属性はcache concurrency strategyを指定します。

14.4.2. 戦略: read only

もしアプリケーションが、読み込みはするが決して永続クラスのインスタンスを修正しないならば、read-onlyキャッシュを使うことができます。これは最も単純で最もパフォーマンスの良い戦略です。クラスタでの使用も完全に安全です。

<class name="eg.Immutable" mutable="false">
    <cache usage="read-only"/>
    ....
</class>

14.4.3. 戦略: read/write

もしアプリケーションがデータを更新する必要があるなら、read-writeキャッシュが適切かもしれません。このキャッシュ戦略は、もしserializable transaction isolationレベルが要求されるなら、決して使うべきではありません。 もしJTA環境でキャッシュが使われるなら、JTAのTransactionManagerを取得する戦略を示すhibernate.transaction.manager_lookup_classプロパティを指定しなければなりません。他の環境ではSession.close()Session.disconnect()がコールされたとき、確実にトランザクションが完了していなければなりません。もしクラスタでこの戦略を使いたいのなら、基となるキャッシュの実装が確実にロックをサポートしていなければなりません。組み込みのキャッシュ・プロバイダはこれをサポートしません

<class name="eg.Cat" .... >
    <cache usage="read-write"/>
    ....
    <set name="kittens" ... >
        <cache usage="read-write"/>
        ....
    </set>
</class>

14.4.4. 戦略: nonstrict read/write

もしアプリケーションがたまにしかデータの更新を必要とせず(つまり、2つのトランザクションが同時に同じitemを更新するようなことがほとんど起きない)、厳密なトランザクションの隔離が要求されないならば、nonstrict-read-writeキャッシュが適当かもしれません。もしキャッシュがJTA環境で使われるなら、hibernate.transaction.manager_lookup_classを指定しなければなりません。他の環境ではSession.close()Session.disconnect()がコールされたとき、確実にトランザクションが完了していなければなりません。

14.4.5. 戦略: transactional

transactionalキャッシュ戦略はJBossのTreeCacheのような、完全にトランザクショナルなキャッシュのサポートを 提供します。そのようなキャッシュはJTA環境だけで使用され、hibernate.transaction.manager_lookup_classを指定しなければなりません。

すべての同時並行性キャッシュ戦略をサポートしているキャッシュ・プロバイダはありません。以下のテーブルはどのプロバイダがどの同時並行性戦略と対応するかを示しています。

表 14.2. 同時並行性キャッシュ戦略のサポート

Cacheread-onlynonstrict-read-writeread-writetransactional
Hashtable (製品用として意図されていません)yesyesyes 
EHCacheyesyesyes 
OSCacheyesyesyes 
SwarmCacheyesyes  
JBoss TreeCacheyes  yes

14.5. Sessionキャッシュの取り扱い

オブジェクトをsave(), update()saveOrUpdate()に渡すとき、そしてload(), find(), iterate(), や filter()を使ってオブジェクトを取り出すときはいつでも、そのオブジェクトはSessionの内部キャッシュに追加されます。続いてflush()がコールされるとき、そのオブジェクトの状態はデータベースと同期されます。 もしこの同期を望まなかったり、膨大な数のオブジェクトを処理し効率的にメモリを扱う必要があるなら、evict()メソッドを使ってキャッシュからオブジェクトやそのコレクションを削除することができます。

Iterator cats = sess.iterate("from eg.Cat as cat"); //巨大なリザルトセット
while ( cats.hasNext() ) {
    Cat cat = (Cat) iter.next();
    doSomethingWithACat(cat);
    sess.evict(cat);
}

もし関連がcascade="all"cascade="all-delete-orphan"でマッピングされているならば、Hibernateは関連するエンティティを自動的にキャッシュから削除します。

Sessionには、インスタンスがセッション・キャッシュに含まれるかどうかを判断するためのcontains()メソッドもあります。

セッション・キャッシュからすべてのオブジェクトを完全に取り除くためには、Session.clear()をコールしてください。

第2レベルのキャッシュのために、SessionFactoryにはインスタンス、クラス全体、コレクション・インスタンスまたはコレクション・ロール全体をキャッシュから削除するメソッドが定義されています。

14.6. クエリ・キャッシュ

クエリ・リザルトセットもキャッシュすることができます。これは同じパラメータで度々実行されるクエリについてのみ役立ちます。 クエリキャッシュを使うためには、まずプロパティhibernate.cache.use_query_cache=trueを設定して使用可能にしておかなければなりません。これにより2つのキャッシュ領域が作成されます。1つはキャッシュされたクエリ・リザルトセットを保持し(net.sf.hibernate.cache.QueryCache)、もう1つは最新のクエリ・テーブルへの更新のタイムスタンプを保持するもの(net.sf.hibernate.cache.UpdateTimestampsCache)です。クエリ・キャッシュはリザルトセットの中のどんなエンティティの状態もキャッシュしないことに注意してください。それは識別子 の値と、バリュー・タイプの結果のみキャッシュします。そのためクエリ・キャッシュは通常、第2レベルのキャッシュと一緒に使われます。

ほとんどのクエリはキャッシュしても意味がないので、デフォルトではクエリはキャッシュされません。キャッシュを有効にするには、Query.setCacheable(true)をコールしてください。そうすればクエリが既存のキャッシュ結果を見つけ、実行時にその結果をキャッシュに追加するようになります。

もしクエリ・キャッシュ終了方針の粒度の良い制御を必要とするなら、Query.setCacheRegion()をコールすることで 特定のクエリに対するキャッシュ領域を指定することができます。

List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
    .setEntity("blogger", blogger)
    .setMaxResults(15)
    .setCacheable(true)
    .setCacheRegion("frontpages")
    .list();

もしクエリがそのクエリキャッシュの範囲をリフレッシュさせるべきなら、Query.setForceCacheRefresh()をコールしてtrueにすることができます。この操作は特にベースとなっているデータが別のプロセスによって更新されるケース(つまりHibernateを通した修正では無いケース)のときに有用であり、アプリケーションがこれらのイベントの知識をベースにして選択的にクエリキャッシュの範囲をリフレッシュできるようになります。これはクエリキャッシュ領域のevictの代替方法です。もしクエリに対してきめの細かいリフレッシュ制御が必要ならば、個々のクエリに新しい領域割り当てル代わりにこの機能を使ってください。