The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
Back to Guides

Quarkusでのトランザクションの使用

QuarkusにはTransaction Managerが付属しており、これを使用してトランザクションを調整してアプリケーションに公開します。永続性を扱う各エクステンションは、これと統合されます。そして、CDIを介して明示的にトランザクションと対話することになります。このガイドでは、これらすべてについて説明します。

設定

これを必要とするエクステンションは単に依存関係として追加するだけなので、ほとんどの場合、設定について心配する必要はありません。例えばHibernate ORMはトランザクションマネージャーを含んでおり、適切に設定されます。

例えば、Hibernate ORMを使用せずに直接トランザクションを使用している場合は、明示的に依存関係として追加する必要があるかもしれません。以下をビルドファイルに追加します:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-narayana-jta</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-narayana-jta")

トランザクションの開始と停止:境界線の定義

トランザクションの境界は、 @Transactional で宣言的に、または QuarkusTransaction でプログラム的に定義できます。 JTA UserTransaction API を直接使用することもできますが、これは QuarkusTransaction よりも使い勝手が悪くなります。

宣言的アプローチ

トランザクションの境界を定義する最も簡単な方法は、エントリーメソッド ( javax.transaction.Transactional ) で @Transactional アノテーションを使用することです。

@ApplicationScoped
public class SantaClausService {

    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;

    @Transactional (1)
    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        Gift gift = childDAO.addToGiftList(child, giftDescription);
        if (gift == null) {
            throw new OMGGiftNotRecognizedException(); (2)
        }
        else {
            santaDAO.addToSantaTodoList(gift);
        }
    }
}
1 このアノテーションは、トランザクションの境界を定義し、トランザクション内でこの呼び出しをラップします。
2 RuntimeException がトランザクションの境界を越えると、トランザクションがロールバックされます。

@Transactional は、メソッドレベルまたはクラスレベルで、すべてのメソッドがトランザクションであることを保証するために、任意の CDI Bean のトランザクション境界を制御するために使用することができます。これにはRESTエンドポイントも含まれます。

@Transactional のパラメーターを使用して、トランザクションを開始するかどうか、どのように開始するかを制御することができます:

  • @Transactional(REQUIRED) (デフォルト): 何も開始されていない場合はトランザクションを開始し、そうでない場合は既存のトランザクションを維持します。

  • @Transactional(REQUIRES_NEW) : 何も開始されていない場合はトランザクションを開始し、既存のトランザクションが開始されている場合はそれを一時停止し、そのメソッドの境界で新しいトランザクションを開始します。

  • @Transactional(MANDATORY) : トランザクションが開始されていない場合は失敗し、そうでない場合は既存のトランザクション内で動作します。

  • @Transactional(SUPPORTS) : トランザクションが開始されている場合、それに参加します。開始されていない場合はトランザクションなしで動作します。

  • @Transactional(NOT_SUPPORTED) : トランザクションが開始されている場合、それを一時停止し、メソッドの境界ではトランザクションなしで動作します。開始されていない場合は、トランザクションなしで動作します。

  • @Transactional(NEVER) : トランザクションが開始されている場合は例外を発生させます。開始されていない場合はトランザクションなしで動作します。

REQUIREDNOT_SUPPORTED が最も便利なものでしょう。これは、あるメソッドがトランザクション内部で実行されるか、外部で実行されるかを決定する方法です。正確な意味については、JavaDocを確認してください。

トランザクション・コンテキストは、予想通り @Transactional メソッドにネストされたすべての呼び出しに伝搬されます (この例では childDAO.addToGiftList()santaDAO.addToSantaTodoList())。ランタイム例外がメソッドの境界を越えない限り、トランザクションはコミットされます。例外が発生したときに強制的にロールバックするかどうかは、 @Transactional(dontRollbackOn=SomeException.class) (または rollbackOn) を使ってオーバーライドできます。

また、プログラムでトランザクションにロールバックのマークを付けることもできます。そのためには TransactionManager をインジェクトします。

@ApplicationScoped
public class SantaClausService {

    @Inject TransactionManager tm; (1)
    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;

    @Transactional
    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        Gift gift = childDAO.addToGiftList(child, giftDescription);
        if (gift == null) {
            tm.setRollbackOnly(); (2)
        }
        else {
            santaDAO.addToSantaTodoList(gift);
        }
    }
}
1 setRollbackOnly のセマンティックを有効にするために、 TransactionManager をインジェクトします。
2 プログラムにより、トランザクションをロールバックするように設定します。

トランザクション設定

トランザクションの高度な設定は、エントリメソッドまたはクラスレベルで標準の @Transactional アノテーションに加えて設定される @TransactionConfiguration アノテーションを使用することで可能です。

@TransactionConfiguration アノテーションでは、タイムアウトのプロパティを秒単位で設定できます。それは、アノテーションされたメソッド内で作成されたトランザクションに適用されます。

このアノテーションは、トランザクションを定義するトップレベルのメソッドにのみ付けることができます。アノテーションされたネストされたメソッドでトランザクションが開始されると、例外がスローされます。

クラスに定義されている場合、 @Transactional でマークされたクラスのすべてのメソッドに定義されているのと同じことになります。メソッドに定義された場合は、クラスに定義された設定よりも優先されます。

リアクティブエクステンション

もし, @Transactional アノテーション付与されたメソッドが、次のようなリアクティブな値を返す場合:

  • CompletionStage (JDKから)

  • Publisher (Reactive-Streamsから)

  • リアクティブ型コンバータを使用して、前の2つの型のうちの1つに変換できる任意の型

これらは、動作が少し異なり、返されたリアクティブ値が終了するまで、トランザクションは終了しません。実際には、返されたリアクティブ値を聞き、それが例外的に終了した場合、トランザクションはロールバックのためにマークされ、リアクティブ値の終了時にのみコミットまたはロールバックされることになります。

これにより、リアクティブメソッドは、リアクティブメソッドが戻るまでではなく、その処理が本当に終了するまで、非同期でトランザクションを処理し続けることができます。

トランザクションコンテキストをリアクティブパイプラインに伝播させる必要がある場合は、 Context Propagationガイドを参照してください。

プログラムアプローチ

QuarkusTransaction の静的メソッドを使用して、トランザクションの境界を定義できます。これは、トランザクションの範囲内でラムダを実行できる関数的なアプローチと、明示的な begin, commit, rollback のメソッドを使用することによる2つの異なるオプションを提供します。

import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.narayana.jta.RunOptions;

public class TransactionExample {

    public void beginExample() {
        QuarkusTransaction.begin();
        //do work
        QuarkusTransaction.commit();

        QuarkusTransaction.begin(QuarkusTransaction.beginOptions()
                .timeout(10));
        //do work
        QuarkusTransaction.rollback();
    }

    public void runnerExample() {
        QuarkusTransaction.requiringNew().run(() -> {
            //do work
        });
        QuarkusTransaction.joiningExisting().run(() -> {
            //do work
        });

        int result = QuarkusTransaction.requiringNew()
                .timeout(10)
                .exceptionHandler((throwable) -> {
                    if (throwable instanceof SomeException) {
                        return RunOptions.ExceptionResult.COMMIT;
                    }
                    return RunOptions.ExceptionResult.ROLLBACK;
                })
                .call(() -> {
                    //do work
                    return 0;
                });
    }
}

The above example shows a few different ways the API can be used.

The first method simply calls begin, does some work and commits it. This created transaction is tied to the CDI request scope, so if it is still active when the request scope is destroyed then it will be automatically rolled back. This removes the need to explicitly catch exceptions and call rollback, and acts as a safety net against inadvertent transaction leaks, however it does mean that this can only be used when the request scope is active. The second example in the method calls begin with a timeout option, and then rolls back the transaction.

The second method shows the use of lambda scoped transactions with QuarkusTransaction.runner(…​); the first example just runs a Runnable within a new transaction, the second does the same but joining the existing transaction (if any), and the third calls a Callable with some specific options. In particular the exceptionHandler method can be used to control if the transaction is rolled back or not on exception.

以下のセマンティクスがサポートされています:

QuarkusTransaction.disallowingExisting()/DISALLOW_EXISTING

もしトランザクションがすでに現在のスレッドに関連していれば、 QuarkusTransactionException が投げられます。そうでなければ、新しいトランザクションが開始され、すべての通常のライフサイクルルールに従います。

QuarkusTransaction.joiningExisting()/JOIN_EXISTING

トランザクションがアクティブでない場合、新しいトランザクションが開始され、メソッドの終了時にコミットされます。例外が発生した場合、 #exceptionHandler(Function) によって登録された例外ハンドラが呼び出され、TX をコミットするかロールバックするかを決定します。既存のトランザクションがアクティブである場合、メソッドは既存のトランザクションのコンテキストで実行されます。例外が発生した場合、例外ハンドラが呼び出されますが、 ExceptionResult#ROLLBACK の結果は、TX がロールバックのみとマークされ、一方 ExceptionResult#COMMIT の結果は、何も行われないという結果になります。

QuarkusTransaction.requiringNew()/REQUIRE_NEW

If an existing transaction is already associated with the current thread then the transaction is suspended, then a new transaction is started which follows all the normal lifecycle rules, and when it’s complete the original transaction is resumed. Otherwise, a new transaction is started, and follows all the normal lifecycle rules.

QuarkusTransaction.suspendingExisting()/SUSPEND_EXISTING

If no transaction is active then these semantics are basically a no-op. If a transaction is active then it is suspended, and resumed after the task is run. The exception handler will never be consulted when these semantics are in use, specifying both an exception handler and these semantics are considered an error. These semantics allows for code to easily be run outside the scope of a transaction.

従来のAPIアプローチ

あまり簡単ではない方法としては、 UserTransaction を注入し、様々なトランザクション・デマケーションのメソッドを使用します。

@ApplicationScoped
public class SantaClausService {

    @Inject ChildDAO childDAO;
    @Inject SantaClausDAO santaDAO;
    @Inject UserTransaction transaction;

    public void getAGiftFromSanta(Child child, String giftDescription) {
        // some transaction work
        try {
            transaction.begin();
            Gift gift = childDAO.addToGiftList(child, giftDescription);
            santaDAO.addToSantaTodoList(gift);
            transaction.commit();
        }
        catch(SomeException e) {
            // do something on Tx failure
            transaction.rollback();
        }
    }
}

@Transactional によって呼び出されたトランザクションと持つメソッドでは、 UserTransaction を使用できません。

トランザクションタイムアウトの設定

デフォルトのトランザクションタイムアウト(トランザクションマネージャが管理するすべてのトランザクションに適用されるタイムアウト)は、 quarkus.transaction-manager.default-transaction-timeout プロパティで設定できます(期間は指定可能です)。

期間のフォーマットは標準の java.time.Duration フォーマットを使用します。詳細は Duration#parse() javadoc を参照してください。

数値で始まる期間の値を指定することもできます。この場合、値が数値のみで構成されている場合、コンバーターは値を秒として扱います。そうでない場合は、 PT が暗黙的に値の前に付加され、標準の java.time.Duration 形式が得られます。

デフォルト値は60秒です。

トランザクションのノード名識別子の設定

Narayanaは、基礎となるトランザクションマネージャーであり、一意のノード名識別子の概念を持っています。これは、複数のリソースを含む XA トランザクションの使用を検討している場合に重要です。

ノード名識別子はトランザクションの識別において重要な役割を果たします。ノード名識別子は、トランザクションが作成される際に、トランザクションIDの作成に使用されます。ノード名識別子に基づいて、トランザクションマネージャーはデータベースまたはJMSブローカに作成されたXAトランザクションのカウンターパートを認識できます。この識別子により、トランザクションマネージャーは復旧時にトランザクションのカウンターパートをロールバックすることが可能になります。

ノード名識別子は、トランザクションマネージャーのデプロイメントごとに一意である必要があります。また、ノード識別子はトランザクションマネージャーの再起動時に変化しない必要があります。

ノード名識別子は、プロパティー quarkus.transaction-manager.node-name にて設定します。

Using @TransactionScoped to bind CDI beans to the transaction lifecycle

You can define beans that live for as long as a transaction, and through CDI lifecycle events perform actions when a transaction starts and ends.

Just assign the transaction scope to such beans using the @TransactionScoped annotation:

@TransactionScoped
public class MyTransactionScopedBean {

    // The bean's state is bound to a specific transaction,
    // and restored even after suspending then resuming the transaction.
    int myData;

    @PostConstruct
    void onBeginTransaction() {
        // This gets invoked after a transaction begins.
    }

    @PreDestroy
    void onBeforeEndTransaction() {
        // This gets invoked before a transaction ends (commit or rollback).
    }
}

Alternatively, if you don’t necessarily need to hold state during the transaction, and just want to react to transaction start/end events, you can simply declare event listeners in a differently scoped bean:

@ApplicationScoped
public class MyTransactionEventListeningBean {

    void onBeginTransaction(@Observes @Initialized(TransactionScoped.class) Object event) {
        // This gets invoked when a transaction begins.
    }

    void onBeforeEndTransaction(@Observes @BeforeDestroyed(TransactionScoped.class) Object event) {
        // This gets invoked before a transaction ends (commit or rollback).
    }

    void onAfterEndTransaction(@Observes @Destroyed(TransactionScoped.class) Object event) {
        // This gets invoked after a transaction ends (commit or rollback).
    }
}
The event object represents the transaction ID, and defines toString()/equals()/hashCode() accordingly.
In listener methods, you can access more information about the transaction in progress by accessing the TransactionManager, which is a CDI bean and can be @Injected.

なぜ常にトランザクションマネージャーを持っているのか?

使いたいところなら、どこでも使えるのでしょうか?

はい、Quarkusアプリケーションでも、IDEでも、テストでも動作します。JTAは一部の人々にとって評判が悪いです。理由はわかりません。これは古いJTA実装ではないとは言っておきましょう。私たちが備えているものは、完全に埋め込み可能で無駄のないものです。

それは2フェーズコミットを行い、私のアプリの速度を遅くしますか?

いえ、それは古い昔話です。基本的にはフリーで提供され、必要に応じて複数のデータソースを含むより複雑なケースにも対応できると考えてください。

読み込み専用の操作をするときにはトランザクションは不要で、その方が早いですよね。

それは誤りです。+ まず第一に、トランザクションの境界を @Transactional(NOT_SUPPORTED) (または NEVER または SUPPORTS ) でマークすることで、トランザクションを無効にしてください。+ 第二に、トランザクションを使用しない方が速いというのは、またしてもおとぎ話です。答えは、それはDBとSQLのSELECT回数に依存します。トランザクションを使用しないということは、DBが単一操作のトランザクションコンテキストを持っていることを意味します。+ 第三に、複数のSELECTを実行する場合は、一つのトランザクションにまとめる方が全てのSELECTに一貫性が保たれます。DBが車のダッシュボードを表しているとすると、残りキロ数と燃料計のレベルなどを見ることができます。それを1つのトランザクションで読み取ることで、それらは整合性を保つことができます。 もし、2つの異なるトランザクションから一方と他方を読み取れば、それらは矛盾することがあります。例えば、権限やアクセス管理に関連するデータを読み取る場合は、より劇的なことが起こる可能性があります。

Hibernateのトランザクション管理APIではなくJTAを好む理由

トランザクションを entityManager.getTransaction().begin() などで手動で管理すると、間違えやすいtry catch finallyの多い醜いコードになってしまいます。また、トランザクションは、JMSと他のデータベースアクセスにも関連しているので、1つのAPIで管理する方が理にかなっています。

JPA永続化ユニットが JTA を使っているのか Resource-level トランザクションを使っているのかわからないので、混乱します

Quarkusで混乱することはありません :)リソースレベルは、管理されていない環境でJPAをサポートするために導入されました。しかし、Quarkusはリーンかつマネージドな環境なので、JTAモードであることを前提に常に安心して利用できます。結果的に、Java SEモードでHibernate ORM + CDI + トランザクションマネージャーを実行することの難しさはQuarkusによって解決されます。