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

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 よりも使い勝手が悪くなります。

宣言的アプローチ

トランザクションの境界を定義する最も簡単な方法は、エントリーメソッドに @Transactional アノテーションを使用することです( jakarta.transaction.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 TransactionExceptionResult.ROLLBACK;
                })
                .call(() -> {
                    //do work
                    return 0;
                });
    }
}

上記の例では、APIの使用方法のいくつか異なる方法を紹介しています。

最初の方法は、単純に begin を呼び出し、何らかの処理を行い、それをコミットするものです。この作成されたトランザクションはCDIリクエストスコープに関連付けられ、リクエストスコープが破棄されたときにまだアクティブであれば、自動的にロールバックされます。これにより、明示的に例外をキャッチしたり rollback を呼び出したりする必要がなくなり、 不用意なトランザクションリークに対するセーフティネットとして機能します。 しかし、これはリクエストスコープがアクティブであるときにしか使えないということになります。メソッド呼び出しの2番目の例は、タイムアウトオプションで開始し、トランザクションをロールバックします。

2つ目のメソッドは、QuarkusTransaction.runner(…​) を使用したラムダスコープのトランザクションの使用を示しています。最初の例では、新しいトランザクション内で Runnable を実行し、2 番目の例では同じことをしますが、既存のトランザクション(もしあれば)に参加し、3 番目の例ではいくつかの特有のオプションを指定して Callable を呼び出します。 特に exceptionHandler メソッドは、例外が発生したときにトランザクションをロールバックするかどうかを制御するために使用することができます。

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

QuarkusTransaction.disallowingExisting()/DISALLOW_EXISTING

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

QuarkusTransaction.joiningExisting()/JOIN_EXISTING

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

QuarkusTransaction.requiringNew()/REQUIRE_NEW

もし既存のトランザクションがすでに現在のスレッドと関連付けられている場合、そのトランザクションは中断され、新しいトランザクションが開始され、すべての通常のライフサイクルルールに従います。その新しいトランザクションが完了すると元のトランザクションが再開されます。現在のスレッドと関連付けられているトランザクションがなければ、新しいトランザクションが開始され、すべての通常のライフサイクルルールに従います。

QuarkusTransaction.suspendingExisting()/SUSPEND_EXISTING

トランザクションがアクティブでない場合、このセマンティックは基本的にノー・オペレーションです。もしトランザクションがアクティブであれば、中断され、タスクの実行後に再開されます。このセマンティックが使用されている場合、例外ハンドラは決して参照されず、例外ハンドラとこのセマンティックの両方を指定することはエラーとみなされます。このセマンティックは、トランザクションの範囲外でコードを簡単に実行することを可能にします。

従来の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 プロパティで設定できます(期間は指定可能です)。

To write duration values, use the standard java.time.Duration format. See the Duration#parse() javadoc for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.

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

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

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

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

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

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

@TransactionScoped を使用し、CDI Bean をトランザクションライフサイクルにバインド

トランザクションと同じ期間だけ有効なBeanを定義し、CDIライフサイクルイベントを通じて、トランザクションの開始と終了時にアクションを実行することができます。

@TransactionScoped アノテーションを使用して、そのようなBeanにトランザクション scope を 割り当てるだけです。

@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).
    }
}

または、必ずしもトランザクション中に状態を保持する必要がなく、トランザクションの開始/終了イベントに反応したいだけであれば、別のスコープを持つ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).
    }
}
event オブジェクトはトランザクション ID を表し、それに応じて toString() / equals() / hashCode() を定義しています。
リスナーメソッドでは、CDI Beanであり、 @Inject 出来る TransactionManager にアクセスすることで、進行中のトランザクションに関するより多くの情報にアクセスすることができます。

Configure storing of Quarkus transaction logs in a database

In cloud environments where persistent storage is not available, such as when application containers are unable to use persistent volumes, you can configure the transaction management to store transaction logs in a database by using a JDBC datasource.

While there are several benefits to using a database to store transaction logs, you might notice a reduction in performance compared with using the file system to store the logs.

Quarkus allows the following JDBC-specific configuration of the object store included in quarkus.transacion-manager.object-store.<property> properties, where <property> can be:

  • type (string): Configure this property to jdbc to enable usage of a Quarkus JDBC datasource for transaction logging. The default value is file-system.

  • datasource (string): Specify the name of the datasource for the transaction log storage. If no value is provided for the datasource property, Quarkus uses the default datasource.

  • create-table (boolean): When set to true, the transaction log table gets automatically created if it does not already exist. The default value is false.

  • drop-table (boolean): When set to true, the tables are dropped on startup if they already exist. The default value is false.

  • table-prefix (string): Specify the prefix for a related table name. The default value is quarkus_.

To work around the current known issue of Agroal having a different view on running transaction checks, set the datasource transaction type for the datasource responsible for writing the transaction logs to disabled:

quarkus.datasource.TX_LOG.jdbc.transactions=disabled

This example uses TX_LOG as the datasource name.

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

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

はい、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で管理する方が理にかなっています。

Jakarta Persistenceの永続化ユニットが JTAResource-level Transactionのどちらを使用しているのかが分からず混乱します

Quarkusで混乱することはありません :)リソースレベルは、非マネージド環境でのjakarta Persistenceをサポートするために導入されました。しかし、Quarkusはリーンな環境であると同時にマネージド環境でもあるので、常にJTAモードであると安全に考えることができます。結果的に、Java SEモードでHibernate ORM + CDI + トランザクションマネージャを実行することの難しさは、Quarkusによって解決されます。