Red Hat Training

A Red Hat training course is available for Red Hat JBoss Web Server

第5章 トランザクションおよび並行性

Hibernate Entity Manager と並行性制御に関する最も重要な点は、非常に理解しやすいことです。Hibernate Entity Manager はロック動作を追加せずに JDBC 接続と JTA リソースを直接使用します。ユーザーは JDBC、ANSI、使用しているデータベース管理システムのトランザクション隔離の仕様について調べてみることが強く推奨されます。Hibernate Entity Manager は自動バージョン機能のみを追加しますが、メモリ内でオブジェクトをロックしたり、データベーストランザクションの隔離レベルを変更したりしません。基本的に、データベースリソースで直接 JDBC (または JTA/CMT) を使用するように Hibernate Entity Manager を使用してください。
Hibernate の並行性制御に関して、最初に EntityManagerFactoryEntityManager の粒度とデータベーストランザクションおよび長い作業単位について説明します。
この章では、特に明示的な記述がない限り、エンティティマネージャと永続コンテキストの概念はほとんど同じです。一方は API およびプログラミングオブジェクトであり、もう一方はスコープの定義です。ただし、重要な違いがあることに注意してください。永続コンテキストは通常、Java EE の JTA トランザクションにバインドされ、拡張されたエンティティマネージャを使用しない限り、永続コンテキストはトランザクション境界 (トランザクションスコープ) で開始および終了します。詳細については、「永続コンテキストスコープ」 を参照してください。

5.1. エンティティマネージャおよびトランザクションスコープ

EntityManagerFactory はすべてのアプリケーションスレッドで共有することを目的とした、作成にコストがかかるスレッドセーフオブジェクトです。これは通常、アプリケーション起動時に 1 度だけ作成されます。
EntityManager は、単一ビジネスプロセスと単一作業単位に対して一度だけ使用し、破棄すべきコストがかからない非スレッドセーフオブジェクトです。EntityManager は、必要でない限り JDBC Connection (または Datasource) を取得しません。したがって、特定の要求に対応するためにデータアクセスが必要であることがわからない場合であっても EntityManager を安全にオープンまたはクローズできます (これは、要求の傍受を使用して以下のいくつかのパターンを実装する場合に重要になります)。
ここでは、データベーストランザクションについても考える必要があります。データベースのロックの競合を削減するために、データベーストランザクションはできるだけ短くする必要があります。データベーストランザクションが長いと、並行性が歌体ロードに対してアプリケーションが対応できなくなります。
作業単位のスコープとは何ですか? 単一の Hibernate EntityManager で複数のデータベーストランザクションに対応できますか、またはこれはスコープの 1 対 1 関係ですか? いつ Session をオープンおよびクローズすべきですか? データベーストランザクション境界をどのように設定しますか?

5.1.1. 作業単位

最初に entitymanager-per-operation アンチパターンを使用しないでください。つまり、単一スレッドで各単一データベースコールに対して EntityManager をオープンおよびクローズしないでください。当然、これはデータベーストランザクションにも当てはまります。アプリケーションのデータベースコールは、計画された順序で行われます。これらのコールはアトミックな作業単位にグループ分けされます (これは、各単一 SQL ステートメント後の自動コミットがアプリケーションで役に立たないことを意味します。このモードはアドホック SQL コンソール作業で使用することを目的としています)。
マルチユーザークライアント/サーバーアプリケーションの最も一般的なパターンは entitymanager-per-request です。このモデルでは、クライアントからの要求はサーバー (JPA 永続レイヤーが実行される) に送信され、新しい EntityManager がオープンされ、すべてのデータベース操作がこの作業単位で実行されます。作業が完了すると (および、クライアントの応答が準備されると)、永続コンテキストとエンティティマネージャがフラッシュされ、クローズされます。また、クライアント要求を処理するために単一のデータベーストランザクションも使用します。2 つの関係は 1 対 1 であり、このモデルは多くのアプリケーションに完全に適合します。
これは、Java EE 環境のデフォルトの JPA 永続モデル (JTA バインドされた、トランザクションスコープ対象の永続コンテキスト) です。挿入 (または検索) されたエンティティマネージャは特定の JTA トランザクションの同じ永続コンテキストを共有します。JPA の利点は、これについて考慮しなくてもよいことであり、エンティティマネージャと、完全に直交するセッション Bean 上のトランザクションスコープの境界からデータアクセスを参照します。
難点は、JPA コンテナ外部の本 (および他の) 動作の実装です。EntityManager とリソースローカルトランザクションを適切に開始および終了する必要があるだけでなく、これらはデータアクセス操作に対してアクセス可能である必要があります。作業単位の境界は、理想的には要求が非 JPA コンテナサーバーに届いたときに (応答が送信される前 (つまり、スタンドアロンサーブレットコンテナを使用する場合は ServletFilter)) 実行されるインターセプタを使用して実装されます。ThreadLocal 変数を使用して要求を処理するスレッドに EntityManager をバインドすることをお薦めします。これにより、このスレッドで実行されるすべてのコードでアクセスが容易になります (静的変数へのアクセスと同様)。選択したデータベーストランザクション境界メカニズムに応じて、トランザクションコンテキストを ThreadLocal 変数に保持することもできます。この実装パターンは Hibernate コミュニティでは ThreadLocal Session および Open Session in View として知られています。Hibernate リファレンスドキュメンテーションに記載された HibernateUtil を簡単に拡張し、このパターンを実装できます。外部ソフトウェアは必要ありません (これは実際には重要なことではありません)。当然、使用している環境でインターセプタを実装し、設定する方法を見つける必要があります。ヒントと例については、Hibernate Web サイトを参照してください。もう一度述べますが、最初の選択肢は JPA コンテナ (理想的には JBoss アプリケーションサーバーなどの軽量でモジュール形式のもの) です。

5.1.2. 長い作業単位

entitymanager-per-request パターンは作業単位を設計する場合にのみ役に立つコンセプトです。多くのビジネスプロセスはデータベースアクセスでインターリーブされたユーザーとの全体の対話を必要します。Web とエンタープライズアプリケーションでは、データベーストランザクションで要求間の待機時間が長いユーザー対話に対応できません。以下の例を考えてください。
  • ダイアログの第一画面が開きます。ユーザーが参照するデータは特定の EntityManager とリソースローカルトランザクションでロードされます。ユーザーはデタッチされたオブジェクトを自由に変更できます。
  • ユーザーが5 分後に "Save" をクリックすると、この変更が永続的になります。また、ユーザーはこの情報を編集する唯一のユーザーとなるため、変更の競合が発生しません。
ユーザーの観点からこの作業単位は長く実行されているアプリケーショントランザクションと呼ばれます。アプリケーションでこれを実装するには多くの方法があります。
最初のナイーブな実装では、ユーザーが考えている間に、同時の変更を防ぎ、分離と原子性を確保するためにデータベースでロックが保持された状態で EntityManager とデータベーストランザクションがオープンのままに場合があります。当然これは、ロックの競合によりアプリケーションが複数の同時ユーザーに対応できなくなるため、アンチパターンであり、ペシミスティックなアプローチです。
当然、アプリケーショントランザクションを実装するには複数のデータベーストランザクションを使用する必要があります。この場合、ビジネスプロセスの分離の維持について、一部はアプリケーション層の責任になります。単一アプリケーショントランザクションには、通常複数のデータベーストランザクションが関係します。これは、これらのいずれかのデータベーストランザクション (少なくとも 1 つ) が更新データを保持し、他のすべてが単にデータを読み取る場合 (複数の要求/応答サイクルにわたるウィザード形式のダイアログなど) にのみ原子的になります。これは、思ったよりも簡単に実装できます (特に、JPA エンティティマネージャと永続コンテキスト機能を使用する場合)。
  • 自動バージョン機能 - エンティティマネージャはユーザーのために自動オプティミスティック並行性制御を実行できます。ユーザーが考えている間に同時の変更が行われたかどうかを自動的に検出できます (通常は、最終リソースローカルトランザクションでのデータの更新時にタイムスタンプのバージョン番号を比較します)。
  • デタッチ済みエンティティ (Detached Entities) - すでに説明された entity-per-request パターンを使用する場合は、ユーザーの考えている間にロードされたすべてのインスタンスがデタッチ済みの状態になります。エンティティマネージャでは、デタッチ済み (変更済み) の状態をマージし、変更を永続化します。このパターンは entitymanager-per-request-with-detached-entities と呼ばれます。自動バージョン機能は同時の変更を分離するために使用されます。
  • 拡張エンティティマネージャ - Hibernate Entity Manager は、2 つのクライアントコール間に基礎となる JDBC 接続から接続解除したり、新しいクライアント要求が発生したときに再接続したりできます。このパターンは entitymanager-per-application-transaction と呼ばれ、マージが不必要になります。拡張永続コンテキストはトランザクション外に行われた変更 (永続化、マージ、削除) を収集および保持します。アクティブなトランザクション内部で行われた次のクライアントコール (通常は、ユーザー対話の最後の操作) はキューに格納されたすべての変更を実行します。自動バージョン機能は同時の変更を分離するために使用されます。
entitymanager-per-request-with-detached-objectsentitymanager-per-application-transaction には利点と欠点があります。これらについては、この章の後半のオプティミスティック並行性制御のコンテキストで説明します。

5.1.3. オブジェクト ID の考慮

アプリケーションは 2 つの異なる永続コンテキストの同じ永続ステータスに同時にアクセスできます。ただし、管理対象クラスのインスタンスは 2 つの永続コンテキスト間で共有されません。したがって、ID には 2 つの異なる表記法があります。
データベース ID
foo.getId().equals( bar.getId() )
JVM ID
foo==bar
特定の永続コンテキスト (つまり、EntityManager のスコープ内) にアタッチされたオブジェクトの場合は、2 つの表記法が同じになり、データベース ID の JVM ID が Hibernate Entity Manager によって保証されます。ただし、アプリケーションが 2 つの異なる永続コンテキストの「同じ」(永続 ID) ビジネスオブジェクトに同時にアクセスできる一方で、2 つのインスタンスは実際には異なります (JVM ID)。競合はフラッシュ/コミット時に自動バージョン機能とオプティミスティックアプローチを使用して解決されます。
このアプローチでは Hibernate とデータベースで並行性について考える必要があります。単一スレッドの作業単位で ID を保証する場合はコストがかかるロックや他の同期手段を必要としないため、最適なスケーラビリティが提供されます。EntityManager ごとの単一スレッドを使用し続ける限り、アプリケーションはビジネスオブジェクトを同期する必要がありません。永続コンテキスト内で、アプリケーションはエンティティを比較するために == を安全に使用できます。
ただし、永続コンテキスト外で == を使用するアプリケーションは、予期しない結果をもたらす場合があります。これは、予期しない場合 (2 つのデタッチ済みインスタンスを同じ Set に配置した場合など) に起こることがあります。両方は同じデータベース ID を持つことができます (つまり、同じ行を表す) が、JVM ID は定義により、デタッチ済みの状態のインスタンスに対して保証されません。開発者は永続クラスの equals() メソッドと hashCode() メソッドをオーバーライドし、オブジェクトの同一性を表す独自の表記法を実装する必要があります。1 つ注意点があります。同一性を実装するためにデータベース ID を使用しないでください。ビジネスキーと、一意の通常は変更不可の属性の組み合わせを使用してください。データベース ID は一時エンティティが永続的になた場合に変わります (persist() 操作のコントラクトを参照)。一時インスタンス (通常は、デタッチ済みのインスタンスとともに使用) が Set で保持された場合は、ハッシュコードを変更すると、 Set のコントラクトが破棄されます。適切なビジネスキーの属性はデータベースプライマリキーほど安定的である必要がなく、オブジェクトが同じ Set に含まれる場合にのみ、安定性を保証する必要があります。この問題の詳細については、Hibernate Web サイトを参照してください。また、これは Hibernate の問題ではなく、単に Java オブジェクト ID と同一性の実装方法の問題であることに注意してください。

5.1.4. 一般的な同一性制御の問題

アンチパターン entitymanager-per-user-session または entitymanager-per-application (当然、このルールの例外はほとんどありません。たとえば、デスクトップアプリケーションでは、永続コンテキストを手動でフラッシュして entitymanager-per-application を使用できます)。以下のいくつかの問題には推奨するパターンが存在します。設計を決定する前に結果について理解してください。
  • エンティティマネージャはスレッドセーフではありません。EntityManager インスタンスが共有される場合は、同時に動作するもの (HTTP 要求、セッション Bean、Swing ワーカーなど) により競合が発生します。Hibernate EntityManagerHttpSession (後で説明) に保持する場合は、Http セッションへのアクセスの同期を考慮してください。考慮しないと、リロードを早くクリックしたユーザーが同時に実行されている 2 つのスレッドで同じ EntityManager を使用することがあります。他の非スレッドセーフのセッションスコープオブジェクトの場合はこれが該当する可能性が非常に高くなります。
  • Entity Manager によりスローされた例外は、データベーストランザクションをロールバックし、EntityManager をすぐにクローズする (詳細は後に説明) 必要があることを意味します。EntityManager がアプリケーションにバインドされた場合は、アプリケーションを停止する必要があります。データベーストランザクションをロールバックしても、ビジネスオブジェクトはトランザクションの開始時の状態に戻りません。つまり、データベースの状態とビジネスオブジェクトは同期されません。例外は復元可能ではなく、ロールバック後に作業単位をやり直す必要があるため、通常これは問題ではありません。
  • 永続コンテキストは、管理対象状態の各オブジェクトをキャッシュします (Hibernate によりダーティ状態が監視およびチェックされる)。つまり、永続コンテキストは、長い間オープンにしたり、ロードするデータが多すぎたりする場合に、OutOfMemoryException を取得するまで継続して肥大化します。この問題の 1 つの解決法は、永続コンテキストの定期的なフラッシュによるバッチ処理です。ただし、大量のデータ操作が必要な場合にはデータベースのストアドプロシージャを使用することを考慮する必要があります。この問題の複数の解決法は 7章バッチ処理 に示されています。ユーザーセッションの間、永続コンテキストをオープンにすると、無効なデータとなる可能性が高くなります (この問題は把握し、適切に処理する必要があります)。