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

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

quarkus-narayana-jta エクステンションは、リンク先 Jakarta Transactions 仕様(旧称 Java Transaction API (JTA))で説明されているように、トランザクションを調停し、アプリケーションに公開するトランザクション・マネージャを提供します 。

Quarkusのトランザクションについて説明する場合、このガイドではJakarta Transactionsのトランザクションスタイルを参照し、 トランザクション という用語のみを使用して説明します。

また、Quarkusは分散トランザクションをサポートしていません。 つまり、 Java Transaction Service (JTS)、REST-AT、WS-Atomic Transactionなど、トランザクションコンテキストを伝播するモデルは、 narayana-jta エクステンションではサポートされません。

設定

これを必要とするエクステンションは単に依存関係として追加するだけなので、ほとんどの場合、設定について心配する必要はありません。例えば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つのタイプのいずれかに変換できるすべてのタイプ

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

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

トランザクションコンテキストをリアクティブパイプラインに伝播させる必要がある場合は、 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 プロパティで設定できます(期間は指定可能です)。

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

数字で始まる簡略化した書式を使うこともできます:

  • 数値のみの場合は、秒単位の時間を表します。

  • 数値の後に ms が続く場合は、ミリ秒単位の時間を表します。

その他の場合は、簡略化されたフォーマットが解析のために java.time.Duration フォーマットに変換されます:

  • 数値の後に hms が続く場合は、その前に PT が付けられます。

  • 数値の後に d が続く場合は、その前に 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 にアクセスすることで、進行中のトランザクションに関するより多くの情報にアクセスすることができます。

Quarkusのトランザクションログをデータベースに保存する設定

アプリケーションコンテナが永続ボリュームを使用できない場合など、永続ストレージを使用できないクラウド環境では、Java Database Connectivity(JDBC)データソースを使用してトランザクションログをデータベースに格納するようにトランザクション管理を構成できます。

しかし、クラウドネイティブアプリでは、トランザクションログを保存するためにデータベースを使用すると、追加の要件があります。 これらのトランザクションを管理する narayana-jta エクステンションが正しく動作するには、安定したストレージ、再利用可能な一意のノード識別子、安定した IP アドレスが必要です。 JDBCオブジェクトストアは安定したストレージを提供しますが、ユーザーは他の2つの要件を満たす方法を計画する必要があります。

Quarkusでは、トランザクションログの保存にデータベースを使用することが適切かどうかを評価したら、 quarkus.transaction-manager.object-store. <property> プロパティで次のようにオブジェクトストアのjdbc固有の設定を行うことができます。 ここで、 <property> は次のとおりです:

  • type (string):トランザクションログの保存にQuarkus JDBCデータソースを使用できるようにするには、このプロパティを jdbc に設定します。 デフォルト値は file-system です。

  • datasource (string):トランザクションログストレージのデータソース名を指定します。 プロパティに値が指定されていない場合、Quarkusは datasource デフォルトのデータソース を使用します。

  • create-table (boolean): true に設定すると、トランザクション・ログ・テーブルが存在しない場合、自動的に作成されます。 デフォルト値は false です。

  • drop-table (boolean): true に設定すると、テーブルが既に存在する場合は起動時に削除されます。 デフォルト値は false です。

  • table-prefix (string):関連テーブル名のプレフィックスを指定します。 デフォルト値は quarkus_ です。

設定の詳細については、Quarkus 全設定オプション リファレンスの Narayana JTA - Transaction manager セクションを参照してください。

追加情報
  • create-table プロパティを true に設定して、初期セットアップ中にトランザクション・ログ・テーブルを作成します。

  • JDBCデータソースとActiveMQ Artemisは、 XAResourceRecovery を自動的に登録します。

    • JDBC データソースは quarkus-agroal の一部であり、 quarkus.datasource.jdbc.transactions=XA を使用する必要があります。

    • ActiveMQ Artemis は quarkus-pooled-jms の一部であり、 quarkus.pooled-jms.transaction=XA を使用する必要があります。

  • アプリケーションのクラッシュや障害が発生した場合にデータの整合性を確保するには、 quarkus.transaction-manager.enable-recovery=true 設定でトランザクションのクラッシュリカバリを有効にします。

トランザクションチェックを実行する際に Agroal が異なるビューを持つ という現在の既知の問題を回避するには、トランザク ションログの書き込みを担当するデータソースのトランザクションタイプを disabled に設定します:

quarkus.datasource.TX_LOG.jdbc.transactions=disabled

この例では、データソース名としてTX_LOGを使用しています。

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

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

はい、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() やその類似を経由して手動でトランザクションを管理すると、最終的に人々が間違えるような試行回数の多い醜いコードになってしまいます。 トランザクションはまた、JMSと他のデータベースアクセスについてですので、単一のAPIはより多くの理にかなっています。

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

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

関連コンテンツ