guice-persist @Transactional をネストしてみる


Guice には、JPA のTransaction 管理用 に guice-persist という
Extension ライブラリが提供されています。

メソッド(またはクラス)に@Transactional アノテーションを付与すると、
トランザクション管理をやってくれるのですが、
@Transactional アノテーションがネストされている場合、
どんな挙動をするのかわからなかったので調べた結果を記載します。

guice-persist の 基本的な使い方は、以下の記事が参考になりました。


動作確認用のサンプルプログラム

動作確認用に使用したサンプル一式はGithub にUPしました。

結論は、Nestできた です。


何故Nestできるのか

当たり前ですが、Nestを考慮した実装になっているからですが、
guice/JpaLocalTxnInterceptor.java at master · google/guice内で、
Nestされている場合は、transactionに対する操作はしないで、
@Transactional アノテーションのついたメソッド呼び出しのみ行っています。

    // Allow 'joining' of transactions if there is an enclosing @Transactional method.
    if (em.getTransaction().isActive()) {
      return methodInvocation.proceed();
    }

Nestされている場合は、em.getTransaction().isActive() がTrueになり、
transactionに対する操作は行われなくなります。

UnitOfWork について

Transactions · google/guice Wiki
@Transactional にともにUnitOfWorkの説明が記載されています。

こちらはTransactionを手動で行いたい場合に、使用するものなのですが、
使い方がわからず、結構長い時間うんうんなってしまったので、
はまった箇所を記載していきます。

実装は以下のように書けます。

  • サンプル実装の抜粋
    @Test
    public void testPersistWithUnitOfWork2() {
        UnitOfWork unitOfwork = injector.getInstance(UnitOfWork.class);
        unitOfwork.begin();
        MyEntityRepository repo = injector.getInstance(MyEntityRepository.class);
        try {
            repo.begin();
            repo.persist("Test 3");
            repo.persist("Test 4");
            repo.persist("Test 5");
            repo.commit();
        } catch (MyEntityRepository.MyRuntimeException ignored) {
        } finally {
            unitOfwork.end();
        }

        unitOfwork = injector.getInstance(UnitOfWork.class);
        unitOfwork.begin();
        repo = injector.getInstance(MyEntityRepository.class);
        try {
            repo.begin();
            assertEquals("Test 3", repo.findByName("Test 3").getName());
            assertEquals("Test 4", repo.findByName("Test 4").getName());
            assertEquals("Test 5", repo.findByName("Test 5").getName());
            repo.commit();
        } finally {
            unitOfwork.end();
        }
    }

プログラムの説明、注意点

UnitOfWork#begin()の呼び出しタイミング

UnitOfWork#begin() の呼び出しで、実装クラスとして、
guice/JpaPersistService.java at master · google/guiceのbegin()メソッドが呼び出されます。
これは、EntityManagerがInjectされる前に、呼び出す必要があり、EntityManagerがInjectされた後の呼び出しの場合、
以下、既に呼び出されているとエラーになります。

java.lang.IllegalStateException: Work already begun on this thread. Looks like you have called UnitOfWork.begin() twice without a balancing call to end() in between.

ですので、@Transactional アノテーションの追加メソッド内で使おうとすると、確実にエラーとなります。
※使おうとすることがまずないと思います。

UnitOfWork#begin() 後は、EntityTransaction#begin()呼び出し > CRUD > EntityTransaction#commit()の順に呼び出し を行う。

EntityTransaction#begin()、EntityTransaction#commit()を明示的に呼び出す必要があります。
呼び出ししないと、@TransactionのついたCRUDメソッドを呼び出した場合は、メソッド単位でコミットされ、
@TransactionのないCRUDメソッドはコミットされない動作になります。
※昔ながらの手動トラン管理になります。

使い所について

@Transaction アノテーションで、80パーセントのケースはうまく使えるかと思いますが、
トランザクション単位でメソッド化しておく必要があるので、
メソッド化が大変な場合や、条件分岐でコミットタイミングが異なる、等
融通を効かせたい場合に使うのかなと思いました。

個人的には、@Transaction でできるだけ書こうかなと思います。

以上です。

コメント