第11章 トランザクションと並行性

Hibernateと同時実行制御について最も重要な点は、容易に理解できることです。 Hibernateは新たなロックの振る舞いを追加しておらず、直接JDBCコネクションとJTAリソースを使用します。 JDBC、ANSI、およびデータベース管理システム(DBMS)のトランザクション分離の仕様を 少し時間をかけて勉強することを強く推奨します。

Hibernateはメモリ内のオブジェクトをロックしません。 アプリケーションは、データベーストランザクションの分離レベルで 定義した振る舞いを期待できます。 トランザクションスコープのキャッシュでもある Session のお陰で、 識別子やクエリにより検索したエンティティはリピータブルリードになります (スカラー値を返すようなレポートクエリは違います)。

バージョニングによる自動的な楽観的同時実行制御に加えて、 SELECT FOR UPDATE 文を使用して、 行を悲観的ロックするための(マイナーな)APIも提供します。 楽観的同時実行制御とこのAPIについては、この章の後のほうで議論します。

データベーストランザクションや長い対話(conversation、ロングトランザクション)だけでなく、 ConfigurationSessionFactory、および Session という粒度でHibernateが行う同時実行制御の議論を始めます。

11.1. sessionスコープとtransactionスコープ

SessionFactory は生成することが高価で、 スレッドセーフなオブジェクトです。 よって、アプリケーションのすべてのスレッドで共有すべきです。 通常、アプリケーションの起動時に、 Configuration インスタンスから1度だけ生成します。

Session は高価ではなく、スレッドセーフなオブジェクトでもありません。 よって、1つの要求や1つの対話、1つの作業単位(unit of work)に対して1度だけ使い、 その後で捨てるべきです。 Session は必要になるまで、 JDBC Connection(もしくは DataSource)を獲得しません。 ゆえに、実際に使用するときまでリソースを消費しません。

この状況を完了させるために、 データベーストランザクションについても考えなければなりません。 データベース内のロックの競合を少なくするために、 データベーストランザクションは可能な限り短くするべきです。 長いデータベーストランザクションは、アプリケーションの高い並列実行性を阻害します。 ゆえに、ユーザーが考えている間(作業単位が完了するまで)データベーストランザクションを 開いたままにするのは、たいていの場合よい設計とはいえません。

作業単位というスコープとは何でしょうか? 1つのHibernate Session は、 いくつかのデータベーストランザクションをまたがることができるでしょうか? または、スコープと一対一の関係でしょうか? いつ Session を開き、閉じるべきでしょうか? そして、データベーストランザクション境界をどのように分けるのでしょうか?

11.1.1. 作業単位(Unit of work)

1つ目は、session-per-operation アンチパターンを使ってはいけません。 すなわち、1つのスレッドの中で、単純なデータベース呼び出しのたびに Session を開いて、閉じてはいけません! もちろん、データベーストランザクションについても同様です。 アプリケーション中のデータベース呼び出しは、 計画されたシーケンス(planned sequence)を使い、 アトミックな作業単位に分類されます。 (1つのSQL文ごとにコミットする自動コミットが、 使われないという意味でもあることに注意してください。 自動コミットは、SQLコンソールでアドホックな作業をする際に使うものです。 Hibernateは直ちに自動コミットモードを無効にします。 もしくは、アプリケーションサーバーが無効化することを期待します。) データベーストランザクションはオプションではありません。 データベースとのすべての通信は、データの読み込みであっても、書き込みであっても、 トランザクションの中で行わなければなりません。 説明すると、データ読み込みに対して、自動コミットは避けるべきです。 なぜなら、多数の小さなトランザクションは、明確に定義された1つの作業単位と比べて、 パフォーマンスがよくなることはありません。 後者は保守性や拡張性もよりすぐれています。

マルチユーザーのクライアント/サーバーアプリケーションの中で、 最もよく使われるパターンは、session-per-request です。 このモデルの中では、 クライアントから(Hibernate永続化層が動作する)サーバーへリクエストが送られ、 新しいHibernate Session が開かれます。 そして、この作業単位の中ですべてのデータベース処理が実行されます。 作業が完了した(そして、クライアントへのレスポンスが準備できた)時点で、 session をフラッシュし、閉じます。 クライアントの要求を処理するために、1つのデータベーストランザクションを使用するでしょう。 Session を開き、閉じる際に、 データベーストランザクションを開始し、コミットします。 二つの関係は一対一です。 このモデルは多くのアプリケーションに完全に適合します。

以降の実装にチャレンジしてください。 Session を持ち、トランザクションを正確に開始し、終わらせるだけでなく、 データアクセス操作も得やすくする必要があります。 リクエストがサーバーにヒットした際と、レスポンスを送信する前に、実行されるinterceptor (例えば、ServletFilter)を使って、作業単位の境界を実装するのが理想的です。 ThreadLocal 変数を使って、 リクエストを処理するスレッドに Session を結びつけることを推奨します。 これにより、スレッド内で実行されるすべてのコードで、 (static変数にアクセスするように)簡単に Session にアクセスできるようになります。 選択したデータベーストランザクション境界の機構次第で、ThreadLocal 変数の中にトランザクションコンテキストを保持するかもしれません。 この実装パターンは、ThreadLocal Session および Open Session in View という名前で知られています。 本ドキュメントで先に見せた HibernateUtil ヘルパークラスを拡張することで、 このパターンを容易に実装できます。 もちろん、interceptor を実装する方法を見つけ、環境をセットアップする必要があります。 HibernateのWebサイトにあるtipsと例を参照してください。

以降の実装にチャレンジしてください。 Hibernateは単純なこのパターンのために、予め組み込まれた 「current session」の管理を提供します。 サーバーリクエストを処理する際はトランザクションを開始しなければなりません。 そして、レスポンスをクライアントに送信する前にトランザクションを終わらせます。 好きな方法で実現できます。一般的な解決策は ServletFilter や サービスメソッドをポイントカットしてAOPインターセプター、 proxy/interception コンテナです。 EJBコンテナはEJBセッションビーンをトランザクション境界として アスペクトをクロスカットする実装の標準的な方法です(CMTによる宣言的)。 プログラムによるトランザクション境界を使うと決めた場合、 簡単に使うため、互換性のあるコードにするために、 この章の後のほうにあるHibernate Transaction APIのほうがよいです。

アプリケーションのコードは、必要なときにどこでも、何回でも、 単に sessionFactory.getCurrentSession() を呼び出すだけで 「現在のセッション」にアクセスできます。 現在のデータベーストランザクションへの セッション を常に取得します。 リソース・ローカルな環境、もしくはJTA環境を構成しなければなりません (項2.5. 「コンテキスト上のセッション」 を参照してください)。

ときどき、「ビューを描画する」まで セッション とデータベーストランザクションのスコープを拡張すると便利なことがあります。 これは、要求の処理と描画のフェーズを分けている サーブレットアプリケーションにおいて特に役立ちます。 独自のインターセプタを実装すれば、 ビューを描画するまでデータベーストランザクションを拡張するのは簡単です。 しかし、コンテナ管理トランザクションのEJBに頼る場合は、簡単にはできません。 なぜなら、ビューの描画を開始する前に、EJBのメソッドがリターンした際に、 トランザクションが完了するためです。 この Open Session in View パターンに関連するヒントと例については、 HibernateのWebサイトやフォーラムを参照してください。

11.1.2. 長い対話

session-per-requestパターンは、作業単位を設計する際に役立つ考えというだけではありません。 多くのビジネスプロセスは、ユーザーとの一連の相互作用全体を要求します。 その相互作用には、データベースアクセスが含まれます。 Webとエンタープライズアプリケーションでは、データベーストランザクションが ユーザとの相互作用にまで渡ることは許されません。 次の例をよく考えてみてください。

  • ダイアログの最初の画面が開き、個々の Session とデータベーストランザクションの中でロードされたデータをユーザーに見せます。 ユーザーはオブジェクトを自由に修正できます。

  • 5分後にユーザーは「Save」をクリックし、修正が永続化されるのを期待します。 また、この情報を編集したのは自分1人だけで、 修正のコンフリクトは発生しないと期待します。

この作業単位を(ユーザーの視点で)長期の 対話 (もしくは、アプリケーショントランザクション )と呼びます。 アプリケーションにこれを実装する方法はたくさんあります。

最初に思いつく実装は、ユーザーが考えている間、Session とデータベーストランザクションを開いたままにしておくことです。 同時に修正されず、分離と原子性が保証されるように、 データベース内のロックは保持したままにします。 もちろん、これはアンチパターンです。 なぜなら、ロックの競合が発生すると、 アプリケーションが同時ユーザー数に応じてスケールアップできなくなるからです。

明らかに、対話を実装するためには、 いくつかのデータベーストランザクションを使用するべきです。 この場合、ビジネスプロセスの分離を維持することは、 アプリケーション層の責務の1つになります。 1つの対話は、 通常いくつかのデータベーストランザクションに及びます。 データベーストランザクションの1つのみ(最後の1つ)が更新したデータを保存し、 他はデータを読むだけであれば、それはアトミックです (例えば、いくつかの要求/応答を繰り返すウィザード形式のダイアログ)。 これは聞くより、実装したほうが簡単です。 Hibernateの機能を使うのであれば、特に簡単です。

  • 自動バージョニング - Hibernateは自動的に楽観的同時実行制御ができます。 ユーザーが考えている間に同時に修正がおきた場合、自動的に検出できます。 通常、対話の終了時にチェックするだけです。

  • 分離(Detached)オブジェクト - すでに議論した session-per-request パターンを使うと決定した場合、 ロードされたすべてのインスタンスは、ユーザが考えている間は、 セッションから分離された状態になります。 オブジェクトをセッションに再追加し、修正を永続化できます。 これを session-per-request-with-detached-objects パターンと呼びます。 自動バージョニングを使うことで、同時に行われる修正を分離できます。

  • 拡張(もしくは、長い)セッション - Hibernateの Session は、 データベーストランザクションをコミットした後、 裏で結びついているJDBCコネクションを切断できます。 そして、クライアントからの新しい要求が発生した際に、再接続できます。 このパターンは、session-per-conversation という名で知られており、オブジェクトをセッションへ再追加することさえ不要にします。 自動バージョニングを使うことで、同時に行われる修正を分離できます。 通常 Session を自動的にフラッシュさせず、 明示的にフラッシュします。

session-per-request-with-detached-objectssession-per-conversation の2つは、 利点と欠点を持っています。 これについては、この章の後のほうで、楽観的同時実行制御の文脈の中で議論します。

11.1.3. オブジェクト識別子を考える

アプリケーションは、2つの異なる Session から 同じ永続状態に同時にアクセスできます。 しかし、2つの Session インスタンスが 永続性クラスの1つのインスタンスを共有することはできません。 ゆえに、識別子には2つの異なる概念があるということになります。

データベース識別子

foo.getId().equals( bar.getId() )

JVM識別子

foo==bar

個々の Session に追加されたオブジェクトにとって (すなわち、1つの Session のスコープの中では)、2つの概念は同じです。 データベース同一性とJVM同一性が一致することを、Hibernateが保証します。 しかし、アプリケーションが2つの異なるセッションから 「同じ」(永続性識別子の)ビジネスオブジェクトに同時にアクセスする限り、 2つのインスタンスは実際に(JVM識別子が)「異なり」ます。 楽観的アプローチによって、(自動バージョニングの)フラッシュ/コミット時に コンフリクトが解決されます。

このアプローチでは、Hibernateとデータベースに同時実行についての心配が残ります。 一方で、最高のスケーラビリティが提供されます。 なぜなら、1スレッドの作業単位の中で一意性が保証されれば、 高価なロックや同期化が不要になるためです。 Session ごとに1つのスレッドを貼り付ける限り、 アプリケーションはビジネスオブジェクトをsynchronizeする必要はありません。 Session 内では、アプリケーションはオブジェクトを比較するために、 == を安全に使用できます。

けれども、Session の外で == を使うアプリケーションは、予期しない結果に遭遇します。 これは予期しない場所で起こりえます。 例えば、2つの分離インスタンスを同じ Set に putしたときなどです。 両方とも同じデータベース識別子を持ちます(すなわち、同じ行を表します)。 しかし、分離状態のインスタンスのJVM識別子は当然保証されません。 開発者は、永続性クラスの equals()hashCode() メソッドをオーバーライドし、オブジェクト等価性の概念を実装すべきです。 警告が1つあります。等価性の実装にデータベース識別子を使わないでください。 ユニークな(普通は不変の)属性の組み合わせであるビジネスキーを使ってください。 もし、一時オブジェクトが永続化された場合、データベース識別子が変わります。 一時オブジェクトを(通常分離インスタンスと共に) Set に保持する場合、ハッシュコードが変わるということは、Set の契約を破るということです。 ビジネスキーのための属性は、データベースの主キーほど安定すべきではないです。 オブジェクトが同じ Set の中にいる間だけ、安定を保証すべきです。 この問題のより徹底的な議論は、HibernateのWebサイトを参照してください。 また、これはHibernateの問題ではなく、単にJavaオブジェクトの識別子や等価性を どのように実装すべきかということです。

11.1.4. 一般的な問題

session-per-user-sessionsession-per-application アンチパターン は使ってはいけません(もちろん、まれに例外があります)。 注意:下記の問題のいくつかは、推奨されるパターンとしても出現します。 設計を決定する前に、裏の意味を理解するようにしてください。

  • Session はスレッドセーフではありません。 HTTPリクエスト、セッションBean、Swingワーカーのように、 同時実行が可能なものが Session インスタンスを共有すると、 競合状態を引き起こします。 (後で議論する)HttpSession の中で Hibernate Session を保持する場合、 HttpSessionへのアクセスを同期化することを考慮すべきです。 さもなければ、ユーザーが十分早くリロードをクリックすると、 同時に走る2つのスレッドの中で、同じ Session が使われます。

  • Hibernateが例外を投げた場合は、データベーストランザクションをロールバックし、 直ちに Session を閉じるべきです(詳細を後で議論します)。 Session がアプリケーションに結び付けられているのであれば、 アプリケーションを停止すべきです。 データベーストランザクションをロールバックしても、ビジネスオブジェクトは トランザクションを開始したときの状態に戻りません。 これは、データベースの状態とビジネスオブジェクトは同期していないことを意味します。 通常これは問題になりません。 なぜなら、例外は回復できないからです。 とにかくロールバックした後にやり直すべきです。

  • Session は永続(persistent)状態のすべてのオブジェクトを キャッシュします(Hibernateは監視し、ダーティ状態かチェックします)。 これは、長い間セッションを開いたままにするか、 非常に多くのデータをロードし続けるかした場合は、 OutOfMemoryExceptionが発生するまで無限に大きくなることを意味します。 解決策の1つは、Session キャッシュを管理するために、 clear()evict() を呼ぶことです。 しかし、大きなデータを処理する必要があるなら、 たぶんストアドプロシージャを考慮するべきでしょう。 いくつかの解決策は、章 13. バッチ処理 で紹介されています。 ユーザーセッションの間、Session を開いたままにするということは、 データが新鮮でなくなる確率が高くなることを意味します。

11.2. データベーストランザクション境界

データベース(もしくはシステム)トランザクションの境界は、常に必要です。 データベーストランザクションの外で、データベースとの通信は起きません (これは自動コミットモードに慣れている多くの開発者を混乱させるかもしれません)。 読み込むだけの操作にでも、いつも明確なトランザクション境界を使用してください。 分離レベルとデータベースの能力次第で、これは必要ないかもしれませんが、 常にトランザクション境界を明示的に指定しても、マイナス面は全くありません。 確かに、1つのデータベーストランザクションは多数の小さなトランザクションより (データの読み込みであっても)パフォーマンスがすぐれています。

J2EE環境に管理されていない状態 (すなわち、スタンドアロン、単純なWebやSwingアプリケーション)でも、 管理された状態でも、Hibernateアプリケーションを実行できます。 管理されていない環境では、Hiberanteがデータベースのコネクションプールを提供します。 アプリケーション開発者は、トランザクション境界を手動で設定しなければなりません。 言い換えると、データベーストランザクションの開始、コミット、ロールバックを 開発者自身が設定する必要があるということです。 通常、管理された環境では、コンテナ管理によるトランザクション(CMT)が提供されます。 例えば、セッションBeanのデプロイメントディスクリプタで宣言的に定義し、 トランザクションを組み立てます。 プログラムによるトランザクション境界はもう必要ありません。

しかしながら、管理されていないリソース・ローカルな環境と JTAに依存したシステム(CMTではなくBMT)の両方に、 永続化層をポータブルに保つのは、しばしば望ましいことです。 デプロイ環境のネイティブのトランザクションシステムを呼び出す Transaction というラッパーAPIをHibernateが提供します。 このAPIを使うかは任意ですが、CMTのセッションBeanを使わないのであれば、使うことを強く推奨します。

通常、Session 終了は、4つの異なるフェーズを含みます。

  • セッションのフラッシュ

  • トランザクションのコミット

  • セッションのクローズ

  • 例外のハンドリング

セッションのフラッシュについては、前のほうですでに議論しました。 管理された環境と管理されていない環境の両方について、 トランザクション境界と例外ハンドリングをもっと詳しく見ていきましょう。

11.2.1. 管理されていない環境

Hibernate永続化層を管理されていない環境で実装する場合は、 通常単純なコネクションプール(すなわちDataSourceではない)によって、 データベースコネクションを制御します。 Hibernateはそのコネクションプールから必要なコネクションを取得します。 セッション/トランザクション制御のイディオムは次のようになります。

// Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
    tx = sess.beginTransaction();

    // do some work
    ...

    tx.commit();
}
catch (RuntimeException e) {
    if (tx != null) tx.rollback();
    throw e; // or display error message
}
finally {
    sess.close();
}

明示的に Sessionflush() を呼び出すべきではなく、 commit() を呼び出すことにより、自動的に同期化処理が実行されます (セッションの 項10.10. 「セッションのフラッシュ」 に依存している)。 close() を呼び出すことにより、セッションの終わりを明確にします。 close() が暗黙的に行う主なことは、 セッションがJDBCコネクションを開放することです。 上記のJavaコードはポータブルであり、管理されていない環境とJTA環境の両方で実行できます。

より適応性のある解決策は、Hibernateに予め組み込まれている 「current session」コンテキスト管理です。 言葉で説明するより下記を見たほうが速いでしょう。

// Non-managed environment idiom with getCurrentSession()
try {
    factory.getCurrentSession().beginTransaction();

    // do some work
    ...

    factory.getCurrentSession().getTransaction().commit();
}
catch (RuntimeException e) {
    factory.getCurrentSession().getTransaction().rollback();
    throw e; // or display error message
}

正規のアプリケーションの中では、このようなコードの切れ端を決して見ないでしょう。 致命的な(システム)例外は、常に「最上位」でキャッチすべきです。 言い換えれば、(永続化層で)Hibernate呼び出しを実行するコードと、 RuntimeException を制御する (通常はクリーンアップと終了のみ行うことができる)コードは、別々の層の中にあります。 Hibernateによるカレントコンテキスト管理は、この設計をかなり単純にします。 必要なのは、SessionFactory にアクセスすることだけです。 例外処理は、この章の後のほうで議論します。

注意:(デフォルトですが) org.hibernate.transaction.JDBCTransactionFactory を選択するべきです。 第2の用例としては、 hibernate.current_session_context_class"thread" とするとよいでしょう。

11.2.2. JTAを使用する

永続化層をアプリケーションサーバー(例えば、EJBセッションBeanの背後)で実行する場合、 Hibernateから取得するすべてのデータソースコネクションは、 自動的にグローバルJTAトランザクションの一部になります。 EJBを使わずに、スタンドアロンのJTA実装を導入することもできます。 JTA統合のために、Hibernateは2つの戦略を提供します。

Bean管理トランザクション(BMT)を使い、Transaction APIを使う場合、 HibernateはアプリケーションサーバーにBMTトランザクションの開始と終わりを告げます。 すなわち、トランザクション管理のコードは、管理されない環境と同じになります。

// BMT idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
    tx = sess.beginTransaction();

    // do some work
    ...

    tx.commit();
}
catch (RuntimeException e) {
    if (tx != null) tx.rollback();
    throw e; // or display error message
}
finally {
    sess.close();
}

トランザクション境界として Session を使いたい場合、 簡単にコンテキストを伝播する機能である getCurrentSession() があるので、 JTAの UserTransaction APIを直接使用すべきでしょう。

// BMT idiom with getCurrentSession()
try {
    UserTransaction tx = (UserTransaction)new InitialContext()
                            .lookup("java:comp/UserTransaction");

    tx.begin();

    // Do some work on Session bound to transaction
    factory.getCurrentSession().load(...);
    factory.getCurrentSession().persist(...);

    tx.commit();
}
catch (RuntimeException e) {
    tx.rollback();
    throw e; // or display error message
}

CMTでは、トランザクション境界をセッションBeanのデプロイメントディスクリプタで定義し、 プログラムでは行いません。 ゆえに、コードは次のように少なくなります。

// CMT idiom
 Session sess = factory.getCurrentSession();

 // do some work
 ...

CMT/EJBの中では、常にロールバックが自動的に実施されます。 なぜなら、セッションビーンのメソッドにより投げられた制御されていない RuntimeException は、グローバルトランザクションを ロールバックするようにコンテナに伝えるためです。 これは、BMTもしくはCMTといっしょにHibernate Transaction APIを使う必要は少しもないという意味です。

Hibernateのトランザクションファクトリを設定する際に、 JTAを直接使う(BMTの)場合は org.hibernate.transaction.JTATransactionFactory を、 CMTセッションビーンの中では org.hibernate.transaction.CMTTransactionFactory を 選択すべきだということに注意してください。 hibernate.transaction.manager_lookup_class をセットすることも思い出してください。 なお、hibernate.current_session_context_class は、 セットしないか(後方互換)、"jta" をセットしてください。

getCurrentSession() オペレーションは、JTA環境では1つの欠点を持ちます。 デフォルトで使われる after_statement コネクションリリースモードを使用する上で、 警告が1つあります。 JTA仕様の愚かな制約のために、 scroll() または iterate() が返した、 閉じられていない ScrollableResults または Iterator インスタンスをHibernateが自動的にクリーンアップすることはできません。 finally ブロックの中で、 ScrollableResults.close() または Hibernate.close(Iterator) を明示的に呼び出して、 裏に潜んだデータベースカーソルを解放 しなければなりません。 (もちろん、多くのアプリケーションでは、JTAかCMTコードで scroll()iterate() の使用を避けるのは容易です。)

11.2.3. 例外ハンドリング

Session が例外(SQLExceptionを含む)を投げた場合、 直ちに、データベーストランザクションをロールバックし、Session.close() を呼び、Session インスタンスを破棄すべきです。 Session のいくつかのメソッドは、 セッションの状態を 矛盾したまま にします。 Hibernateが投げた例外を、回復できるものとして扱うことはできません。 finally ブロックの中で close() を呼んで、 Session を確実に閉じてください。

HibernateException は、Hibernate永続化層の中で発生する多くのエラーを ラップする、検査されない例外です(Hibernateの古いバージョンは違いました)。 私たちの意見は、アプリケーション開発者に回復不可能な例外を 下層でキャッチすることを強要すべきではないということです。 多くのシステムでは、検査されない例外と致命的な例外は、 コールスタックの最初のフレームの1つ(例えば、最上位の層で)でハンドリングし、 エラーメッセージをアプリケーションユーザーに表示します (もしくは、他の適切な処理を実施します)。 Hibernateは、HibernateException 以外の検査されない例外も 投げることに注意してください。 これらもまた、回復不可能であり、適切な処理を実施すべきです。

Hibernateは、データベースとの対話中に投げられた SQLExceptionJDBCException でラップします。 実は、例外をより意味のある JDBCException のサブクラスに 変換しようと試みます。 元の SQLException は、JDBCException.getCause() によりいつでも得られます。 Hibernateは、SessionFactory に追加されている SQLExceptionConverter を使い、SQLException を 適当な JDBCException サブクラスに変換します。 デフォルトでは、SQLExceptionConverter は設定されているSQL方言により 定義されます。 一方で、独自の実装に差し替えることもできます (詳細は、SQLExceptionConverterFactory クラスのJavadocを参照してください)。 標準的な JDBCException のサブタイプを下記に示します。

  • JDBCConnectionException - 基礎となるJDBC通信のエラーを表します。

  • SQLGrammarException - 発行するSQLの文法もしくは構文の問題を表します。

  • ConstraintViolationException - 何らかの形式の完全性制約違反を表します。

  • LockAcquisitionException - 要求された操作を実施するのに必要なロックレベルを得る際のエラーを表します。

  • GenericJDBCException - 他のカテゴリに一致しなかった一般的な例外です。

11.2.4. トランザクションのタイムアウト

EJBのような管理された環境が提供するきわめて重要な特徴の1つは、 トランザクションのタイムアウトです。 これは管理されていないコードには提供できません。 トランザクションタイムアウトは、不品行なトランザクションが ユーザーにレスポンスを返さないまま、無期限にリソースを使い続けない ことを保障します。 管理された環境(JTA)の外では、Hibernateはこの機能をフルに提供できません。 しかしながら、Hibernateは次のようなデータアクセス操作の制御くらいはできます。 データベースレベルのデッドロックや大きなリザルトセットを返すクエリを 定義されたタイムアウトによって確実に制限します。 管理された環境では、HibernateはトランザクションタイムアウトをJTAに委譲します。 この機能は、Hibernateの Transaction オブジェクトに よって抽象化されています。

Session sess = factory.openSession();
try {
    //set transaction timeout to 3 seconds
    sess.getTransaction().setTimeout(3);
    sess.getTransaction().begin();

    // do some work
    ...

    sess.getTransaction().commit()
}
catch (RuntimeException e) {
    sess.getTransaction().rollback();
    throw e; // or display error message
}
finally {
    sess.close();
}

CMTビーンの中では setTimeout() を 呼び出せないことに注意してください。 トランザクションタイムアウトは宣言的に定義されるべきです。

11.3. 楽観的同時実行制御

高い並列性と高いスケーラビリティの両方を実現するアプローチは、 バージョニングを使った楽観的同時実行制御のみです。 更新の衝突を見つけるために(および、更新が失われるのを防ぐために)、 バージョン番号もしくはタイムスタンプを使って、バージョンをチェックします。 Hibernateは、楽観的同時実行を行うアプリケーションコードを書くための アプローチを3つ提供します。 私たちが見せるユースケースは、長い対話を持ちますが、 バージョンチェックはまだ1つのデータベーストランザクションの中で更新を失うことを防ぐ 利点も持っています。

11.3.1. アプリケーションによるバージョンチェック

Hibernateにほとんど助けてもらわずに実装するケースです。 データベースとのやり取りは、それぞれ新しい Session の中で起こります。 開発者は、すべての永続性インスタンスを操作する前に、 データベースから再読み込みする責務があります。 このアプローチでは、対話トランザクションの分離を守るために、 アプリケーション自身がバージョンチェックを行う必要があります。 このアプローチは、データベースアクセスの中では、最も非効率です。 エンティティEJBと最も似ているアプローチです。

// foo is an instance loaded by a previous Session
session = factory.openSession();
Transaction t = session.beginTransaction();

int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() ); // load the current state
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");

t.commit();
session.close();

<version> を使って、version プロパティをマッピングします。 Hibernateは、エンティティがダーティである場合、フラッシュし、 その間に version プロパティを自動的にインクリメントします。

もちろん、データの並列性が低い環境で運用しており、バージョンチェックが不要なら、 このアプローチを使い、バージョンチェックをスキップするだけです。 その場合は、長い対話には、 「最後にコミットしたものが勝つ」 がデフォルトの戦略でしょう。 このアプローチは、アプリケーションのユーザーを混乱させるかもしれないことを 心に留めて置いてください。それは、エラーメッセージや競合した変更をマージする機会が ないまま、更新を失う経験をするためです。

確かに、マニュアルによるバージョンチェックは、些細な儀式だけで実行できますが、 多くのアプリケーションにとって実用的ではありません。 しばしば、1つのインスタンスだけでなく、 修正されたオブジェクトの完全なグラフをチェックしなければなりません。 Hibernateは、設計パラダイムとして、拡張 Session か 分離されたインスタンスを自動的にバージョンチェックします。

11.3.2. 拡張セッションと自動バージョニング

1つの Session インスタンスとその永続性インスタンスは、 session-per-conversation として知られる、 対話全体で使われます。 Hibernateはフラッシュする際に、インスタンスのバージョンをチェックします。 同時に修正されたことを検出すると、例外を投げます。 この例外をキャッチして扱うのは、開発者の責任です (一般的な選択肢は、変更をマージするか古くないデータでビジネス対話を 再スタートする機会をユーザーに提供することです)。

ユーザーの対話を待っているときは、 Session を基礎となるJDBCコネクションから切り離します。 このアプローチは、データベースアクセスの中では、最も効率的です。 アプリケーションは、バージョンチェックや分離されたインスタンスを再追加すること に関心を持つ必要はありません。また、あらゆるデータベーストランザクションの中で インスタンスを再読み込みする必要はありません。

// foo is an instance loaded earlier by the old session
Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction

foo.setProperty("bar");

session.flush();    // Only for last transaction in conversation
t.commit();         // Also return JDBC connection
session.close();    // Only for last transaction in conversation

foo オブジェクトは、自分をロードした Session をまだ知っています。 古いセッションの上で新しいデータベーストランザクションを開始することで、 新しいコネクションを取得し、そのセッションが再開されます。 データベーストランザクションをコミットすることで、 セッションからJDBCコネクションを切断し、コネクションをプールに返します。 再接続した後、更新していないデータのバージョンチェックを強制するために、 他のトランザクションにより更新されているかもしれないオブジェクトに関して、 LockMode.READ をつけて Session.lock() を呼び出すことができます。 更新して いる データをロックする必要はありません。 通常、拡張 SessionFlushMode.NEVER をセットします。 最後のデータベーストランザクションの周期でのみ、 対話の中で変更されたすべてを実際に永続化させるためです。 ゆえに、最後のデータベーストランザクションのみ flush() オペレーションを含みます。そして、対話を終わらせるために、 セッションも close() します。

ユーザーが考慮中に、格納することができないくらい Session が大きいのであれば、このパターンは問題があります。 例えば、HttpSession は可能な限り小さく保つべきです。 Session は(強制的に)1次キャッシュでもあり、 ロードしたオブジェクトをすべて保持します。 おそらく、リクエスト/レスポンスのサイクルが数回であれば、この戦略が使えます。 1つの対話のためだけに Session を使うべきです。 なぜなら、すぐに新鮮でないデータを持つためです。

(Hibernateの以前のバージョンは、明示的な Session の 切断と再接続が必要だったことに注意してください。 これらのメソッドは非推奨になりました。 なぜなら、トランザクションの開始と終了は同じ効果があるためです。)

切断した Session を永続化層の近くで保持すべきであることに 注意してください。 言い換えると、3層環境の中で Session を保持するために、 EJBステートフルセッションBeanを使ってください。 HttpSession に格納するために、Web層に転送しないでください (別の層へのシリアライズもしないでください)。

拡張セッションパターン(もしくは、session-per-conversation)は、 自動的なカレントセッションコンテキスト管理を実施するより難しい。 このために、あなたは CurrentSessionContext の実装を供給する必要があります。 Hibernate Wikiにある例を参照してください。

11.3.3. デタッチされたオブジェクトと自動バージョニング

新しい Sessionにより、永続化ストア(訳注:DB)との対話が発生します。 また一方、同じ永続性インスタンスが、データベースとの対話ごとに再利用されます。 アプリケーションは、元々は他の Session でロードされ、 デタッチされたインスタンスの状態を操作します。 そして、Session.update() もしくは、Session.saveOrUpdate()Session.merge() を使って、それらのインスタンスを再追加します。

// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();

この場合もやはり、Hibernateはフラッシュする際に、インスタンスのバージョンをチェックします。 更新の競合が発生した場合には、例外を投げます。

オブジェクトが修正されていないことを確信している場合は、 update() の代わりに、LockMode.READ を使って、 lock() を呼び出すこともできます (すべてのキャッシュを迂回し、バージョンチェックを実施します)。

11.3.4. 自動バージョニングのカスタマイズ

マッピングの optimistic-lock 属性に false を設定することにより、特定のプロパティやコレクションのために 自動バージョンインクリメントを無効にできます。 プロパティがダーティであっても、バージョンをインクリメントしません。

レガシーのデータベーススキーマは、しばしば固定的であり、変更できません。 または、他のアプリケーションが同じデータベースにアクセスしなければならず、 そのアプリケーションはバージョン番号やタイムスタンプさえ操作する方法を知りません。 どちらの場合も、テーブルの特定のカラムを当てにして、バージョニングを行えません。 バージョンやタイムスタンプのプロパティをマッピングせずに、バージョンチェックさせるために、 <class> マッピングに optimistic-lock="all" を 指定してください。 行のすべてのフィールドの状態を比較するようになります。 これは、Hibernateが古い状態と新しい状態を比較できる場合に、 理論的に動作するだけであることに注意してください。 例えば、session-per-request-with-detached-objects ではなく、 1つの長い Session を使う場合です。

ときどき、行われた変更が重ならない限り、同時に行われた変更を受け入れることができます。 <class> マッピングに optimistic-lock="dirty" を設定した場合、フラッシュする際に、Hibernateはダーティフィールドのみを比較します。

専用のバージョン/タイムスタンプのカラムを使う場合、 もしくはすべて/ダーティのフィールドを比較する場合どちらであっても、 Hibernateはエンティティごとに1つの UPDATE 文を (適切な WHERE 節と共に)使い、 バージョンチェックと情報の更新を行います。 関連するエンティティの再追加をカスケードするために、 連鎖的な永続化を使用した場合、不必要な更新を実行するかもしれません。 これは通常問題になりません。 しかし、分離したインスタンスを変更していなくとも、 データベースの on update トリガーが実行されるかもしれません。 <class> マッピングに select-before-update="true" を設定することによって、 この振る舞いをカスタマイズできます。 確実に変更されたかを確認するために、行を更新する前に、 必ずインスタンスを SELECT します。

11.4. 悲観的ロック

ユーザがロック戦略に悩むのに多くの時間を費やすことを意図していません。 通常は、JDBCコネクションに分離レベルを指定し、 単にデータベースにすべての仕事をさせれば十分です。 しかしながら、高度なユーザは、排他的な悲観的ロックを獲得することか、 新しいトランザクションが開始される際にロックを再獲得することを ときどき望むかもしれません。

Hibernateはいつもデータベースのロックの仕組みを使います。 メモリ内のオブジェクトを決してロックしません!

LockMode クラスは、Hibernateが獲得できる異なるロックレベルを定義します。 以下の仕組みにより、ロックを獲得できます。

  • LockMode.WRITE は、 Hibernateが行を更新もしくは挿入する際に自動的に得られます。

  • LockMode.UPGRADE は、 データベースでサポートされている文法 SELECT ... FOR UPDATE を使った、明示的なユーザー要求により得られるかもしれません。

  • LockMode.UPGRADE_NOWAIT は、 Oracle で SELECT ... FOR UPDATE NOWAIT を使った、 明示的なユーザー要求により得られるかもしれません。

  • LockMode.READ は、 Repeatable ReadもしくはSerializableの分離レベルで、データを読んだ際に自動的に得られます。 おそらく、明示的なユーザー要求により、再取得されます。

  • LockMode.NONE は、ロックしないことを表します。 Transaction の終わりに、 すべてのオブジェクトはこのロックモードに切り替わります。 update()saveOrUpdate() を呼び出すことによって、 セッションに関連付けられたオブジェクトも、このロックモードで出発します。

「明示的なユーザー要求」とは、下記の方法の1つで言い表せます。

  • LockMode を指定した Session.load() の呼び出し。

  • Session.lock() の呼び出し。

  • Query.setLockMode() の呼び出し。

UPGRADE もしくは UPGRADE_NOWAIT が指定された Session.load() が呼び出され、 かつ要求されたオブジェクトがセッションによってまだロードされていなかった場合は、 SELECT ... FOR UPDATE を使って、オブジェクトがロードされます。 load() で呼び出されたオブジェクトが、 要求されているより制限が少ないロックですでにロードされていた場合は、 Hibernateはそのオブジェクトのために、lock() を呼び出します。

指定されたロックモードが READ もしくは、UPGRADEUPGRADE_NOWAIT だった場合、Session.lock() は、バージョン番号のチェックを実施します。 (UPGRADE もしくは UPGRADE_NOWAIT の場合、 SELECT ... FOR UPDATE が使われます。)

データベースが要求されたロックモードをサポートしていない場合、 Hibernateは(例外を投げる代わりに、)適切な代わりのモードを使います。 これは、アプリケーションがポータブルであることを保証します。

11.5. コネクション開放モード

Hibernateのレガシー(2.x)のJDBCコネクション管理に関する振る舞いは、 最初に必要とした際に Session がコネクションを得るというものでした。 そして、セッションが閉じられるまで、そのコネクションを保持しました。 Hibernate 3.xは、セッションにJDBCコネクションをどのように制御するかを伝える コネクション開放モードという概念を導入しました。 以降の議論は、構成された ConnectionProvider を通して提供される コネクションに適切であることに注意してください。 異なる開放モードは、org.hibernate.ConnectionReleaseMode に 列挙された値により確認されます。

  • ON_CLOSE - 本質的に上記で述べたレガシーの振る舞いです。 Hibernateセッションは最初にJDBCアクセスを実行する必要がある際にコネクションを得ます。 そして、セッションが閉じられるまで、コネクションを保持します。

  • AFTER_TRANSACTION - org.hibernate.Transaction が完了した後、 コネクションを開放します。

  • AFTER_STATEMENT (積極的な開放とも呼ばれる) - すべてのステートメントがそれぞれ実行された後、コネクションが開放されます。 ステートメントがセッションに関連するリソースを開いたままにする場合は、 この積極的な開放はスキップされます。 今のところ、これが起こるのは org.hibernate.ScrollableResults が使われる場合のみです。

コンフィグレーションパラメータの hibernate.connection.release_mode は、使用する開放モードを指定するために使います。 指定できる値は次の通りです。

  • auto (デフォルト) - これを選択すると org.hibernate.transaction.TransactionFactory.getDefaultReleaseMode() メソッドによって返される開放モードに委譲されます。 このメソッドは、 JTATransactionFactoryにはConnectionReleaseMode.AFTER_STATEMENTを返し、 JDBCTransactionFactoryにはConnectionReleaseMode.AFTER_TRANSACTIONを返します。 このデフォルトの振る舞いを変えてうまくいった試しがありません。 それは、この設定値が原因で起こる障害は、 ユーザコードの中でバグや間違った条件になりやすいからです。

  • on_close - ConnectionReleaseMode.ON_CLOSEを使います。 この設定は後方互換のために残されていますが、使わないことを強く勧めます。

  • after_transaction - ConnectionReleaseMode.AFTER_TRANSACTIONを使います。 この設定はJTA環境の中では使うべきではありません。 ConnectionReleaseMode.AFTER_TRANSACTIONを指定し、自動コミットモードの中では、 開放モードがAFTER_STATEMENTであるかのように、コネクションは開放されることに注意してください。

  • after_statement - ConnectionReleaseMode.AFTER_STATEMENTを使います。 さらに、設定された ConnectionProvider は、 この設定 (supportsAggressiveRelease()) をサポートするかどうか を調べるために使用します。 もしそうでない場合、開放モードはConnectionReleaseMode.AFTER_TRANSACTION にリセットされます。 この設定は次の環境でのみ安全です。 それは、ConnectionProvider.getConnection() を呼び出すたびに 基盤となるJDBCコネクションが同じものを取得できるか、 同じコネクションが得られることが問題とならない自動コミット環境の中です。