1. 概要

Java Transaction API(より一般的にはJTAとして知られています)は、Javaでトランザクションを管理するためのAPIです。これにより、リソースに依存しない方法でトランザクションを開始、コミット、およびロールバックできます。

JTAの真の力は、複数のリソースを管理する能力にあります(つまり、 データベース、メッセージングサービス)を単一のトランザクションで実行します。

このチュートリアルでは、概念レベルでJTAを理解し、ビジネスコードが一般的にJTAとどのように相互作用するかを確認します。

2. ユニバーサルAPIと分散トランザクション

JTAは、ビジネスコードへのトランザクション制御(開始、コミット、ロールバック)の抽象化を提供します。

この抽象化がない場合は、各リソースタイプの個々のAPIを処理する必要があります。

たとえば、こののようなJDBCリソースを処理する必要があります。 同様に、JMSリソースには、類似しているが互換性のないモデルが含まれている場合があります。

JTAを使用すると、さまざまなタイプの複数のリソースを一貫性のある調整された方法で管理できます

APIとして、JTAはトランザクションマネージャーによって実装されるインターフェースとセマンティクスを定義します。 実装は、NarayanaAtomikosなどのライブラリによって提供されます。

3. サンプルプロジェクトの設定

サンプルアプリケーションは、銀行アプリケーションの非常に単純なバックエンドサービスです。 2つのサービスがあります。 BankAccountService AuditService 2つの異なるデータベースを使用する 。 これらの独立したデータベースは、トランザクションの開始、コミット、またはロールバック時に調整する必要があります

まず、サンプルプロジェクトでは、SpringBootを使用して構成を簡素化します。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

最後に、各テストメソッドの前に、 AUDIT_LOG を空のデータで初期化し、データベースACCOUNTを2行で初期化します。

+-----------+----------------+
| ID        |  BALANCE       |
+-----------+----------------+
| a0000001  |  1000          |  
| a0000002  |  2000          |
+-----------+----------------+

4. 宣言型トランザクション境界

JTAでトランザクションを操作する最初の方法は、@Transactionalアノテーションを使用することです。 より詳細な説明と構成については、この記事を参照してください。

ファサードサービスメソッドに注釈を付けましょう executeTranser() @Transactional。 これは、 トランザクションマネージャートランザクションを開始するには

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    ...
}

ここでメソッド executeTranser() 2つの異なるサービスを呼び出します。 AccountService AuditService。 これらのサービスは2つの異なるデータベースを使用します。

executeTransfer()が戻ると、トランザクションマネージャーはそれがトランザクションの終了であることを認識し、両方のデータベースにコミットします

tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000001"))
  .isEqualByComparingTo(BigDecimal.valueOf(500));        
assertThat(accountService.balanceOf("a0000002"))
  .isEqualByComparingTo(BigDecimal.valueOf(2500));

TransferLog lastTransferLog = auditService
  .lastTransferLog();
assertThat(lastTransferLog)
  .isNotNull();        
assertThat(lastTransferLog.getFromAccountId())
  .isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId())
  .isEqualTo("a0000002"); 
assertThat(lastTransferLog.getAmount())
  .isEqualByComparingTo(BigDecimal.valueOf(500));

4.1. 宣言型境界でのロールバック

メソッドの最後で、 executeTransfer()は口座残高を確認し、ソース資金が不足している場合はRuntimeExceptionをスローします。

@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
    bankAccountService.transfer(fromAccontId, toAccountId, amount);
    auditService.log(fromAccontId, toAccountId, amount);
    BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
    if(balance.compareTo(BigDecimal.ZERO) < 0) {
        throw new RuntimeException("Insufficient fund.");
    }
}

最初の@Transactionalを過ぎた未処理のRuntimeExceptionは、トランザクションを両方のデータベースにロールバックします。 実際には、残高よりも大きい金額で転送を実行すると、ロールバックが発生します

assertThatThrownBy(() -> {
    tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(10000));
}).hasMessage("Insufficient fund.");

assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditServie.lastTransferLog()).isNull();

5. プログラムによるトランザクションの境界

JTAトランザクションを制御する別の方法は、UserTransactionを介してプログラムで行うことです。

次に、 executeTransfer()を変更して、トランザクションを手動で処理しましょう。

userTransaction.begin();
 
bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
if(balance.compareTo(BigDecimal.ZERO) < 0) {
    userTransaction.rollback();
    throw new RuntimeException("Insufficient fund.");
} else {
    userTransaction.commit();
}

この例では、 begin()メソッドが新しいトランザクションを開始します。 バランスの検証が失敗した場合は、 rollback()を呼び出します。これにより、両方のデータベースがロールバックされます。 それ以外の場合は、 commit() の呼び出しにより、両方のデータベースに変更がコミットされます。

commit() rollback()の両方が現在のトランザクションを終了することに注意することが重要です。

最終的に、プログラムによる境界設定を使用すると、きめ細かいトランザクション制御の柔軟性が得られます。

6. 結論

この記事では、JTAが解決しようとする問題について説明しました。 コード例は、アノテーションを使用してトランザクションを制御し、プログラムで、単一のトランザクションで調整する必要がある2つのトランザクションリソースを含むことを示しています。

いつものように、コード例はGitHubにあります。