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

宣言的アプローチ

トランザクションの境界を定義する最も簡単な方法は、エントリーメソッド ( 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 lambdaExample() {
        QuarkusTransaction.run(() -> {
            //do work
        });


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

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

2つ目の例は、ラムダスコープのトランザクションを使用しています。1つ目は単にトランザクション内で Runnable 、2つ目はいくつかの特定のオプションをつけて Callable を実行します。特に、 exceptionHandler メソッドは、例外発生時にトランザクションをロールバックするかどうかを制御するために使用され、 semantic メソッドは、既存のトランザクションが既に開始されている場合の動作を制御します。

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

DISALLOW_EXISTING

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

JOIN_EXISTING

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

REQUIRE_NEW

これはデフォルトのセマンティックです。もし既存のトランザクションがすでに現在のスレッドと関連している場合、そのトランザクションは中断され、現在のトランザクションが完了すると再開されます。新しいトランザクションは、既存のトランザクションが中断された後に開始され、すべての通常のライフサイクルルールに従います。

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 を参照してください。

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

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

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

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

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

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

ノード名識別子は、プロパティー quarkus.transaction-manager.node-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で管理する方が理にかなっています。

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

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