Hibernate.orgCommunity Documentation
Hibernate と同時実行制御について最も重要な点は、容易に理解できることです。 Hibernate は新たなロックの振る舞いを追加しておらず、直接 JDBC コネクションと JTA リソースを使用します。 JDBC 、 ANSI 、およびデータベース管理システム(DBMS)のトランザクション分離の仕様を少し時間をかけて勉強することを強く推奨します。
Hibernate はメモリ内のオブジェクトをロックしません。アプリケーションは、データベーストランザクションの分離レベルで定義した振る舞いを期待できます。トランザクションスコープのキャッシュでもある Session
のお陰で、識別子やクエリにより検索したエンティティはリピータブルリードになります(スカラー値を返すようなレポートクエリは違います)。
バージョニングによる自動的な楽観的同時実行制御に加えて、 SELECT FOR UPDATE
文を使用して、行を悲観的ロックするための(マイナーな) API も提供します。楽観的同時実行制御とこの API については、この章の後のほうで議論します。
データベーストランザクションや長い対話(conversation、ロングトランザクション)だけでなく、 Configuration
、SessionFactory
、および Session
という粒度で Hibernate が行う同時実行制御の議論を始めます。
SessionFactory
は生成することが高価で、スレッドセーフなオブジェクトです。よって、アプリケーションのすべてのスレッドで共有すべきです。通常、アプリケーションの起動時に、 Configuration
インスタンスから1度だけ生成します。
Session
は高価ではなく、スレッドセーフなオブジェクトでもありません。よって、1つの要求や1つの対話、1つの作業単位(unit of work)に対して1度だけ使い、その後で捨てるべきです。 Session
は必要になるまで、 JDBC Connection
(もしくは DataSource
)を獲得しません。ゆえに、実際に使用するときまでリソースを消費しません。
この状況を完了させるために、データベーストランザクションについても考えなければなりません。データベース内のロックの競合を少なくするために、データベーストランザクションは可能な限り短くするべきです。長いデータベーストランザクションは、アプリケーションの高い並列実行性を阻害します。ゆえに、ユーザーが考えている間(作業単位が完了するまで)データベーストランザクションを開いたままにするのは、たいていの場合よい設計とはいえません。
作業単位というスコープとは何でしょうか?1つの Hibernate Session
は、いくつかのデータベーストランザクションをまたがることができるでしょうか?または、スコープと一対一の関係でしょうか?いつ Session
を開き、閉じるべきでしょうか?そして、データベーストランザクション境界をどのように分けるのでしょうか?
First, let's define a unit of work. A unit of work is a design pattern described by Martin Fowler as 「 [maintaining] a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems. 」[PoEAA] In other words, its a series of operations we wish to carry out against the database together. Basically, it is a transaction, though fulfilling a unit of work will often span multiple physical database transactions (see 「長い対話」). So really we are talking about a more abstract notion of a transaction. The term "business transaction" is also sometimes used in lieu of 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
を開き、閉じる際に、データベーストランザクションを開始し、コミットします。二つの関係は一対一です。このモデルは多くのアプリケーションに完全に適合します。
以降の実装にチャレンジしてください。 Hibernate は単純なこのパターンのために、予め組み込まれた "current session" の管理を提供します。サーバーリクエストを処理する際はトランザクションを開始しなければなりません。そして、レスポンスをクライアントに送信する前にトランザクションを終わらせます。好きな方法で実現できます。一般的な解決策は ServletFilter
やサービスメソッドをポイントカットして AOP インターセプター、 proxy/interception コンテナです。 EJB コンテナは EJB セッション Bean をトランザクション境界としてアスペクトをクロスカットする実装の標準的な方法です( CMT による宣言的)。プログラムによるトランザクション境界を使うと決めた場合、簡単に使うため、互換性のあるコードにするために、この章の後のほうにある Hibernate Transaction
API のほうがよいです。
Your application code can access a "current session" to process the request by calling sessionFactory.getCurrentSession()
. You will always get a Session
scoped to the current database transaction. This has to be configured for either resource-local or JTA environments, see 「コンテキスト上のセッション」.
ときどき、「ビューを描画する」まで セッション
とデータベーストランザクションのスコープを拡張すると便利なことがあります。これは、要求の処理と描画のフェーズを分けているサーブレットアプリケーションにおいて特に役立ちます。独自のインターセプタを実装すれば、ビューを描画するまでデータベーストランザクションを拡張するのは簡単です。しかし、コンテナ管理トランザクションの EJB に頼る場合は、簡単にはできません。なぜなら、ビューの描画を開始する前に、 EJB のメソッドがリターンした際に、トランザクションが完了するためです。この Open Session in View パターンに関連するヒントと例については、 Hibernate の Web サイトやフォーラムを参照してください。
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-objects と session-per-conversation の2つは、利点と欠点を持っています。これについては、この章の後のほうで、楽観的同時実行制御の文脈の中で議論します。
アプリケーションは、2つの異なる Session
から同じ永続状態に同時にアクセスできます。しかし、2つの Session
インスタンスが永続性クラスの1つのインスタンスを共有することはできません。ゆえに、識別子には2つの異なる概念があるということになります。
foo.getId().equals( bar.getId() )
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 オブジェクトの識別子や等価性をどのように実装すべきかということです。
session-per-user-session と session-per-application アンチパターンは使ってはいけません(もちろん、まれに例外があります)。注記:下記の問題のいくつかは、推奨されるパターンとしても出現します。設計を決定する前に、裏の意味を理解するようにしてください。
Session
はスレッドセーフではありません。 HTTP リクエスト、セッション Bean 、 Swing ワーカーのように、同時実行が可能なものが Session
インスタンスを共有すると、競合状態を引き起こします。(後で議論する) HttpSession
の中で Hibernate Session
を保持する場合、 HttpSession へのアクセスを同期化することを考慮すべきです。さもなければ、ユーザーが十分早くリロードをクリックすると、同時に走る2つのスレッドの中で、同じ Session
が使われます。
Hibernate が例外を投げた場合は、データベーストランザクションをロールバックし、直ちに Session
を閉じるべきです (詳細を後で議論します) 。 Session
がアプリケーションに結び付けられているのであれば、アプリケーションを停止すべきです。データベーストランザクションをロールバックしても、ビジネスオブジェクトはトランザクションを開始したときの状態に戻りません。これは、データベースの状態とビジネスオブジェクトは同期していないことを意味します。通常これは問題になりません。なぜなら、例外は回復できないからです。とにかくロールバックした後にやり直すべきです。
The Session
caches every object that is in a persistent state (watched and checked for dirty state by Hibernate). If you keep it open for a long time or simply load too much data, it will grow endlessly until you get an OutOfMemoryException. One solution is to call clear()
and evict()
to manage the Session
cache, but you should consider a Stored Procedure if you need mass data operations. Some solutions are shown in 15章バッチ処理. Keeping a Session
open for the duration of a user session also means a higher probability of stale data.
データベース (もしくはシステム) トランザクションの境界は、常に必要です。データベーストランザクションの外で、データベースとの通信は起きません (これは自動コミットモードに慣れている多くの開発者を混乱させるかもしれません) 。読み込むだけの操作にでも、いつも明確なトランザクション境界を使用してください。分離レベルとデータベースの能力次第で、これは必要ないかもしれませんが、常にトランザクション境界を明示的に指定しても、マイナス面は全くありません。確かに、1つのデータベーストランザクションは多数の小さなトランザクションより (データの読み込みであっても) パフォーマンスがすぐれています。
J2EE 環境に管理されていない状態 (すなわち、スタンドアロン、単純な Web や Swing アプリケーション)でも、管理された状態でも、 Hibernate アプリケーションを実行できます。管理されていない環境では、 Hiberante がデータベースのコネクションプールを提供します。アプリケーション開発者は、トランザクション境界を手動で設定しなければなりません。言い換えると、データベーストランザクションの開始、コミット、ロールバックを開発者自身が設定する必要があるということです。通常、管理された環境では、コンテナ管理によるトランザクション (CMT) が提供されます。例えば、セッション Bean のデプロイメントディスクリプタで宣言的に定義し、トランザクションを組み立てます。プログラムによるトランザクション境界はもう必要ありません。
しかしながら、管理されていないリソースローカルな環境と JTA に依存したシステム (CMT ではなく BMT) の両方に、永続化層をポータブルに保つのは、しばしば望ましいことです。デプロイ環境のネイティブのトランザクションシステムを呼び出す Transaction
というラッパー API を Hibernate が提供します。この API を使うかは任意ですが、 CMT のセッション Bean を使わないのであれば、使うことを強く推奨します。
通常、 Session
終了は、4つの異なるフェーズを含みます:
セッションのフラッシュ
トランザクションのコミット
セッションのクローズ
例外のハンドリング
セッションのフラッシュについては、前の方で既に議論しました。管理された環境と管理されていない環境の両方について、トランザクション境界と例外ハンドリングをもっと詳しく見ていきましょう。
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();
}
You do not have to flush()
the Session
explicitly: the call to commit()
automatically triggers the synchronization depending on the FlushMode for the session. A call to close()
marks the end of a session. The main implication of close()
is that the JDBC connection will be relinquished by the session. This Java code is portable and runs in both non-managed and JTA environments.
より適応性のある解決策は、 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"
とするとよいでしょう。
永続化層をアプリケーションサーバー (例えば、 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 の中では、常にロールバックが自動的に実施されます。なぜなら、セッション Bean のメソッドにより投げられた制御されていない RuntimeException
は、グローバルトランザクションをロールバックするようにコンテナに伝えるためです。 これは、 BMT もしくは CMT と一緒に Hibernate Transaction
API を使う必要はまったくないということを意味し、トランザクションにバインドする「現在の」セッションを自動伝搬できます。
Hibernate のトランザクションファクトリを設定する際に、 JTA を直接使う (BMTの) 場合は org.hibernate.transaction.JTATransactionFactory
を、 CMT セッション Bean の中では 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()
の使用を避けるのは容易です。)
Session
が例外 (SQLException
を含む) を投げた場合、直ちに、データベーストランザクションをロールバックし、 Session.close()
を呼び、 Session
インスタンスを破棄すべきです。 Session
のいくつかのメソッドは、セッションの状態を 矛盾したまま にします。 Hibernate が投げた例外を、回復できるものとして扱うことはできません。 finally
ブロックの中で close()
を呼んで、 Session
を確実に閉じてください。
HibernateException
は、 Hibernate 永続化層の中で発生する多くのエラーをラップする、検査されない例外です ( Hibernate の古いバージョンは違いました) 。私たちの意見は、アプリケーション開発者に回復不可能な例外を下層でキャッチすることを強要すべきではないということです。多くのシステムでは、検査されない例外と致命的な例外は、コールスタックの最初のフレームの1つ (例えば、最上位の層で) でハンドリングし、エラーメッセージをアプリケーションユーザーに表示します (もしくは、他の適切な処理を実施します) 。 Hibernate は、HibernateException
以外の検査されない例外も投げることに注意してください。これらもまた、回復不可能であり、適切な処理を実施すべきです。
Hibernate は、データベースとの対話中に投げられた SQLException
を JDBCException
でラップします。実は、例外をより意味のある JDBCException
のサブクラスに変換しようと試みます。元の SQLException
は、 JDBCException.getCause()
によりいつでも得られます。 Hibernate は、 SessionFactory
に追加されている SQLExceptionConverter
を使い、 SQLException
を適当な JDBCException
サブクラスに変換します。デフォルトでは、 SQLExceptionConverter
は設定されている SQL 方言により定義されます。一方で、独自の実装に差し替えることもできます (詳細は、 SQLExceptionConverterFactory
クラスの Javadoc を参照してください)。標準的な JDBCException
のサブタイプを下記に示します。
JDBCConnectionException
- 基礎となる JDBC 通信のエラーを表します。
SQLGrammarException
- 発行する SQL の文法もしくは構文の問題を表します。
ConstraintViolationException
- 何らかの形式の完全性制約違反を表します。
LockAcquisitionException
- 要求された操作を実施するのに必要なロックレベルを得る際のエラーを表します。
GenericJDBCException
- 他のカテゴリに一致しなかった一般的な例外です。
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 Bean の中では setTimeout()
を呼び出せないことに注意してください。トランザクションタイムアウトは宣言的に定義されるべきです。
高い並列性と高いスケーラビリティの両方を実現するアプローチは、バージョニングを使った楽観的同時実行制御のみです。更新の衝突を見つけるために(および、更新が失われるのを防ぐために)、バージョン番号もしくはタイムスタンプを使って、バージョンをチェックします。 Hibernate は、楽観的同時実行を行うアプリケーションコードを書くためのアプローチを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
か分離されたインスタンスを自動的にバージョンチェックします。
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()
を呼び出すことができます。更新して いる データをロックする必要はありません。通常、拡張 Session
に FlushMode.MANUAL
をセットします。最後のデータベーストランザクションの周期でのみ、対話の中で変更されたすべてを実際に永続化させるためです。ゆえに、最後のデータベーストランザクションのみ flush()
オペレーションを含みます。そして、対話を終わらせるために、セッションも close()
します。
ユーザーが考慮中に、格納することができないくらい Session
が大きいのであれば、このパターンは問題があります。例えば、 HttpSession
は可能な限り小さく保つべきです。 Session
は (強制的に) 1次キャッシュでもあり、ロードしたオブジェクトをすべて保持します。おそらく、リクエスト/レスポンスのサイクルが数回であれば、この戦略が使えます。1つの対話のためだけに Session
を使うべきです。なぜなら、すぐに新鮮でないデータを持つためです。
Hibernate の以前のバージョンは、明示的な Session
の切断と再接続が必要だったことに注意してください。これらのメソッドは非推奨になりました。なぜなら、トランザクションの開始と終了は同じ効果があるためです。
切断した Session
を永続化層の近くで保持すべきであることに注意してください。言い換えると、3層環境の中で Session
を保持するために、 EJB ステートフルセッション Bean を使ってください。 HttpSession
に格納するために、 Web 層に転送しないでください (別の層へのシリアライズもしないでください)。
拡張セッションパターン (もしくは、 session-per-conversation ) は、自動的なカレントセッションコンテキスト管理を実施するより難しい。このために、あなたは CurrentSessionContext
の実装を供給する必要があります。 Hibernate Wiki にある例を参照してください。
新しい 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()
を呼び出すこともできます (すべてのキャッシュを迂回し、バージョンチェックを実施します)。
マッピングの 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
します。
ユーザーがロック戦略に悩むのに多くの時間を費やすことを意図していません。通常は、 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
もしくは、 UPGRADE
、 UPGRADE_NOWAIT
だった場合、 Session.lock()
は、バージョン番号のチェックを実施します。 (UPGRADE
もしくは UPGRADE_NOWAIT
の場合、 SELECT ... FOR UPDATE
が使われます。)
データベースが要求されたロックモードをサポートしていない場合、 Hibernate は(例外を投げる代わりに、)適切な代わりのモードを使います。これは、アプリケーションがポータブルであることを保証します。
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 コネクションが同じものを取得できるか、同じコネクションが得られることが問題とならない自動コミット環境の中です。
製作著作 © 2004 Red Hat, Inc.