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を使用せずに直接トランザクションを使用している場合は、明示的に依存関係として追加する必要があるかもしれません。以下をビルドファイルに追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-narayana-jta</artifactId>
</dependency>
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)
: トランザクションが開始されている場合は例外を発生させます。開始されていない場合はトランザクションなしで動作します。
REQUIRED
や NOT_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;
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 TransactionExceptionResult.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();
}
}
}
|
トランザクションタイムアウトの設定
デフォルトのトランザクションタイムアウト(トランザクションマネージャが管理するすべてのトランザクションに適用されるタイムアウト)は、 quarkus.transaction-manager.default-transaction-timeout
プロパティで設定できます(期間は指定可能です)。
期間の値を書くには、標準の 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|
デフォルト値は60秒です。
トランザクションのノード名識別子の設定
Narayanaは、基礎となるトランザクションマネージャーであり、一意のノード識別子の概念を持っています。これは、複数のリソースを含む XA トランザクションの使用を検討している場合に重要です。
ノード名識別子はトランザクションの識別において重要な役割を果たします。ノード名識別子は、トランザクションが作成されるときに、トランザクションIDの作成に使用されます。ノード名識別子に基づいて、トランザクションマネージャーはデータベースまたはJMSブローカに作成されたXAトランザクションのカウンターパートを認識することができます。この識別子により、トランザクションマネージャーはリカバリ中にトランザクションのカウンターパートをロールバックすることが可能になる。
ノード名識別子は、トランザクションマネージャーのデプロイメントごとに一意である必要があります。また、ノード識別子はトランザクションマネージャーの再起動時に変化しない必要があります。
ノード名識別子は、プロパティー quarkus.transaction-manager.node-name
にて設定します。
The node name cannot be longer than 28 bytes.
To automatically shorten names longer than 28 bytes, set Shortening is implemented by hashing the node name, encoding the hash to Base64 and then truncating the result. As with all hashes, the resulting shortened node name could potentially conflict with another shortened node name, but it is very unlikely. |
@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 が異なるビューを持つ という現在の既知の問題を回避するには、トランザク ションログの書き込みを担当するデータソースのトランザクションタイプを 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によって解決されました。