トランザクション属性REQUIRES_NEWの落とし穴

トランザクション属性としてREQUIRES_NEWを指定したセッションBeanのメソッドを、別のセッションBeanから何度も呼ぶと、2回目の呼び出しの時にトランザクションタイムアウトが起こるまで、処理が固まってしまう現象が起きた。

簡略化すると、以下のようなセッションBean。

@Stateless
public class SampleBean implements Sample {
    @EJB
    private Sample2 sample2;

    public void method1() {
        ... // 適当な処理
        sample2.method2();
        ... // 適当な処理
        sample2.method2();  // ここでトランザクションタイムアウトまで処理が止まる?
        ... // 適当な処理
    }
}

@Stateless
public class SampleBean2 implements Sample2 {
   
    @TransactionAttribute(REQUIRES_NEW)
    public void method2() {
        ... // データ更新
    }
}

EJBコンテナの外から、SampleBean#method1()が呼ばれる。

EJB3.xのトランザクション

EJB3.1トランザクションは、特に何も指定しなければ、コンテナ管理トランザクションにてトランザクション属性としてREQUIREDが適用して処理される。なので、呼び出されたメソッドの開始がトランザクションの開始、メソッドの終了がトランザクションの終了となり、その内部で呼ばれたセッションBeanのメソッドでは、同じトランザクションが使われる。

で、EJBコンテナの外から呼ばれたセッションBeanのメソッドの中で、例外の有無によらず、あるトランザクションだけは必ずコミットしたいといった場合(典型的な例はログのDB書き込みなど)は、呼び出すメソッドのトランザクション属性をREQUIRES_NEWにすることで、対応できる。

REQUIRES_NEWの場合、一時的にトランザクションを中断して新しいトランザクションで実行し、コミット/ロールバックを行った後に中断したトランザクションが再開される。

この辺りはアノテーションだけで制御できるので、非常に柔軟。結果的に、ソースコード中にトランザクションに関する記述をしなくてよくなる。

上記の例でもそういう風に、SampleBean#method1()内で例外が起こるかどうかに関係なく、常にSampleBean2#method2()の内容はコミットされるようにしようとした。

REQUIRES_NEWを使う時の注意点

原因究明のために細かい注意点を調べてみる。

そうすると、REQUIRES_NEWを使う時に一番気をつけないとならないのは、同じクラス内のメソッドを呼ぶ場合、直接呼んでしまうと呼び出し側のメソッドのトランザクション属性は適用されない、という点が分かった*1

上記の記事にあるように、同じクラス内のメソッドに対してトランザクション属性を効かせたければ、自分自身をインジェクションするなどの対処が必要になる。

そういや再帰呼び出しとかしてた気がするな…。

自己解決(6/14)

色々試して自己解決した。

REQUIRES_NEWするのが2回目以降とかどうとかは別に関係なかった。

セッションBeanのメソッドの再帰呼び出しだったり、for updateを使った悲観的排他制御だったりが色々混ざってきてしまった*2ので、プログラムを整理した。

データの登録、更新を行うBeanを2つ程度に集約し、その2つのBeanのトランザクション属性としてREQUIRES_NEWを指定することで、期待する動作をしてくれるようになった。

アノテーションで色々と指定するのは個人的には好きだし、ユニットテストも書きやすくなるけど、後のフェーズでのトラブルシューティングに余計な時間を取られる危険もあるなぁと痛感。基本的な仕様はきちんと抑えた上で、コーディングの指針とかも事前にしっかり決めておかないとならないね。

*1:「適用されない」というのは正確でないかも。実際の挙動は、無視されるのか、エラーになるのか、それ以外になるのかは試してない…

*2:実は素直にJPAを使っているわけではなく、本ブログでも何度か触れたDomaというORマッパーを使っている、