第10章 トランザクションと同時並行性

Hibernate自体はデータベースではなく、ライトウェイトなオブジェクト/リレーショナル・ マッピング・ツールです。 そのためトランザクション管理は、ベースとなっているデータベース・コネクションへ任されます。 コネクションがJTAに登録されていれば、 Session が実行する操作は、より大きなJTAトランザクションの一部です。 オブジェクト指向の意味を追加したJDBCのシン・アダプタとして、Hibernateを捉えることもできます。

10.1. Configuration、Session、Factory

SessionFactory は生成することが高価で、 スレッドセーフなオブジェクトです。 これはすべてのアプリケーション・スレッドで共有すべきです。 それに対して Session は高価ではなく、スレッドセーフでもありません。 これは1つのビジネス・プロセスに対して1度だけ使われて、捨てられるべきです。 例えばサーブレット・ベースのアプリケーションでHibernateを使うとき、 サーブレットは以下のようにして SessionFactory を取得できます。

SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");

サービス・メソッドへのコールのたびに、新しく Session が作成され、 flush() され、コネクションを commit() し、 close() して、最後には捨てられます。 (SessionFactory は、JNDIや、 staticな Singleton のヘルパ変数に取っておくこともできます。)

ステートレス・セッション・ビーンでも同じようなアプローチを使えます。 ビーンは setSessionContext()SessionFactory を取得します。 各ビジネス・メソッドで Session が作成され、flush() され、 close() されます。 当然、アプリケーションではコネクションを commit() すべきではありません。 (JTAに任せると、コンテナ管理トランザクションで、データベースが自動的に関連付けられます。)

以前述べたHiberante Transaction APIを使うことにします。 Hibernate Transaction の1つの commit() は、 状態をフラッシュし、(JTAトランザクションの特別な扱いで) データベース・コネクションをコミットします。

flush() の意味を確実に理解してください。 フラッシュはメモリ内部の変更を永続ストアに同期させますが、逆は成り立ち ません。 すべてのHibernateJDBCコネクション/トランザクションに対して、 コネクションのトランザクション分離レベルがHibernateが実行する すべての操作に適用されることに注意してください。

次節では、トランザクション・アトミシティを確実にするもう一つの方法を議論します (バージョン付けを使います)。 これは「高度な」方法なので、注意して使わなければなりません。

10.2. スレッドとコネクション

Hibernate Sessionを作成するときは、以下の規則を守るべきです:

  • データベース・コネクションに対して、複数の同時並行 SessionTransaction インスタンスを作成しない。

  • データベース・トランザクションに対して複数の Session を作成するときは、 極めて慎重に行う。 Session 自体はロードされたオブジェクトに加えられた更新を覚えていますが、 他の Session は古いデータを見てしまうかもしれません。

  • Session はスレッドセーフでは ありません 。 そのため、2つの同時並行スレッドで、同じ Session にアクセスしてはいけません。 普通 Session は、作業単位(unit-of-work)1つにすぎません。

10.3. オブジェクト・アイデンティティの考慮

アプリケーションは2つの異なる仕事単位で、同じ永続状態に並行にアクセスできます。 しかし永続クラスのインスタンスを、2つの Session インスタンスで共有することはできません。 つまりアイデンティティに対する、2つの異なる考え方があります:

データベース・アイデンティティ

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

JVMアイデンティティ

foo==bar

オブジェクトが1つの 特定の Session に関連付けられている場合は、 2つの考え方は等価です。 しかし2つの異なるSessionで、アプリケーションが「同じ」(永続アイデンティティ) ビジネス・オブジェクトに並行にアクセスする場合は、 2つのインスタンスは実際には「違うもの」です(JVMアイデンティティ)。

この方法はHibernateとデータベースに、同時並行性を任せます。 Session に対して1つのスレッドに関連付けられているか、 オブジェクト・アイデンティティである限りは、 アプリケーションがビジネス・オブジェクトを同期させる必要は全くありません。 (1つの Session で、アプリケーションはオブジェクトの比較のために == を安全に使うことができます。)

10.4. optimistic同時並行性制御

ビジネス・プロセスの多くはデータベース・アクセスとユーザ・インタリーブの 一連の相互作用全体を必要とします。 ウェブやエンタープライズアプリケーションでは、データベース・トランザクションが ユーザとの対話に及ぶことは許されません。

ビジネス・プロセスの分離の維持は、アプリケーション層の責務の一部になります。 そのためこのプロセスを 長い間実行される アプリケーション・トランザクション と呼びます。 1つのアプリケーション・トランザクションは、 普通いくつかのデータベース・トランザクションに及びます。 もしこれらのデータベース・トランザクション(の最後の1つ)が更新されたデータを格納すれば、 よりアトミックになり、他のすべてのトランザクションは単にデータを読むだけになります。

高い同時並行性とスケーラビリティを持つ唯一の方法は、 バージョン付けを使うoptimisticな同時並行性制御です。 Hibernateではoptimistic同時並行性を使うアプリケーション・コードを書くための方法が 3種類用意されています。

10.4.1. 自動バージョン付けを使う長いSession

1つの Session インスタンスとその永続インスタンスは、 アプリケーション・トランザクション全体に渡って使われます。

Session はバージョン付けを使った楽観的ロックを使い、 データベース・トランザクションの多くが1つの論理的なアプリケーション・トランザクションに 現れることを確実にします。 ユーザとの対話を待つ間、Session はベースとなる JDBCコネクションと切断されています。 この方法は、データベース・アクセスという点では最も効率的です。 アプリケーションは自身のバージョンをチェックすることも、 切り離したインスタンスの再接続も考慮する必要はありません。

// fooは以前のSessionでロードしていたインスタンス
session.reconnect();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.disconnect();

foo オブジェクトは、それをロードした Session をまだ知っています。 Session がJDBCコネクションを持つとすぐに、 オブジェクトに対する変更をコミットします。

Session がユーザが考える時間で格納するには大きすぎるなら、 このパターンは問題になります。 例えば HttpSession はできるだけ小さく保たなければなりません。 Session も(強制的な)ファーストレベル・キャッシュで、 すべてのロードされたオブジェクトを含んでいるように、 この戦略はリクエスト/レスポンス・サイクルが少ないときだけに使うことができます。 Session> はすぐに古いデータを持つようになるので、 これを強くおすすめします。

10.4.2. 自動バージョン付けを使う多くのSession

永続ストアとの対話はそれぞれ、新しい Session で起こります。 しかし同じ永続インスタンスが、データベースとの各対話で再利用されます。 元は他の Session でロードされた切り離されたオブジェクトの状態を、 アプリケーションが操作します。 そして Session.update()Session.saveOrUpdate() を使い、 それらを「再接続」します。

// fooは以前のSessionでロードしていたインスタンス
foo.setProperty("bar");
session = factory.openSession();
session.saveOrUpdate(foo);
session.flush();
session.connection().commit();
session.close();

オブジェクトが更新されていないことが確実なら、 update() の代わりに lock() を、 そして LockMode.READ を使うことができます。 (すべてのキャッシュを使わずに、バージョン・チェックを実行します)

10.4.3. アプリケーション・バージョン・チェック

データベースとの対話は新しい Session で起こります。 そこでは操作する前にデータベースからすべての永続インスタンスをリロードします。 この方法は、アプリケーション・トランザクションの分離を確実にするために、 アプリケーションが自分のバージョンチェックを実行することを強制します。 (もちろんHibernateはあなたのためにバージョン番号を 更新 します。) この方法はデータベース・アクセスの点では最も効率が悪いです。 これはエンティティEJBに最もよく似ています。

// fooは以前のSessionでロードしていたインスタンス
session = factory.openSession();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() );
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
session.flush();
session.connection().commit();
session.close();

もちろん、もしデータ同時並行性の低い環境でバージョン・チェックが必要なければ、 この方法を使いバージョン・チェックをスキップできます。

10.5. Sessionの切断

上で述べた最初の方法は、ユーザの考慮時間に及ぶビジネス・プロセス全体に対して、 1つの Session を維持するものです。 (例えば、サーブレットはユーザの HttpSession で1つの Session を保持します。) パフォーマンスを考慮すると以下のようにすべきです。

  1. Transaction (またはJDBCコネクション)をコミットし、

  2. JDBCコネクションから Session を切断する

ユーザの行動を待つ前に、これらを行います。 Session.disconnect() メソッドは、 JDBCコネクションからSessionを切断し、 (もしコネクションを用意していなければ)プールに返します。

Session.reconnect() は新しいコネクションを取得し (または自分で用意し)、Sessionを再開します。 再接続したあと、更新していないデータのバージョン・チェックを強制するために、 他のトランザクションで更新されたどんなオブジェクトに対しても Session.lock() をコールできます。 更新中の どんなデータもロックする必要はありません。

以下はその例です:

SessionFactory sessions;
List fooList;
Bar bar;
....
Session s = sessions.openSession();

Transaction tx = null;
try {
    tx = s.beginTransaction();

    fooList = s.find(
    	"select foo from eg.Foo foo where foo.Date = current date"
        // DB2のdate関数を使います
    );
    bar = (Bar) s.create(Bar.class);

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    s.close();
    throw e;
}
s.disconnect();

その後で:

s.reconnect();

try {
    tx = s.beginTransaction();

    bar.setFooTable( new HashMap() );
    Iterator iter = fooList.iterator();
    while ( iter.hasNext() ) {
        Foo foo = (Foo) iter.next();
        s.lock(foo, LockMode.READ);    // fooが古くないことをチェックします
        bar.getFooTable().put( foo.getName(), foo );
    }

    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    throw e;
}
finally {
    s.close();
}

これから TransactionSession の間がどのようなmany-to-oneであるか、 Session がどのようにアプリケーションとデータベースの間の 対話を表現するかがわかるでしょう。 Transaction は対話をデータベース・レベルの仕事単位に分解します。

10.6. 悲観的ロック

ユーザがロックの戦略について時間をかけて考えるとは期待していません。 普通は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の 分離レベルで、Hibernateがデータを読み込むときに自動的に取得されます。 明示的なユーザ・リクエストによる再取得かもしれません。

  • LockMode.NONE はロックされていないことを表します。 すべてのオブジェクトは Transaction の終了時に、 このロック・モードに切り替えられます。 update() または saveOrUpdate() のコールを通して Sessionに関連付けられたオブジェクトも、このロック・モードで開始されます。

「明示的なユーザ・リクエスト」は以下の方法の1つとして表現されます:

  • LockMode を指定した、Session.load() のコール。

  • Session.lock() のコール。

  • Query.setLockMode() のコール。

Session.load()UPGRADE または UPGRADE_NOWAIT でコールされ、 リクエストされたオブジェクトがまだSessionによってロードされていなければ、 オブジェクトは SELECT ... FOR UPDATE を使ってロードされます。 リクエストしたものよりも低い制限のロックですでにロードされていたオブジェクトに対して load() がコールされれば、 Hibernateはそのオブジェクトに対して lock() をコールします。

ロック・モードが READ, UPGRADE, UPGRADE_NOWAIT と指定されていれば、 Session.lock() はバージョン番号をチェックします ( UPGRADE または UPGRADE_NOWAIT の場合は、 SELECT ... FOR UPDATE が使われます。)。

データベースがリクエストしたロック・モードに対応していなければ、 Hibernateは(例外をスローする代わりに)他の適切なモードを使います。 これによりアプリケーションがポータブルになります。