宣言的トランザクションのロールバック

前のセクションでは、アプリケーションで宣言的にクラス(通常はサービスレイヤークラス)のトランザクション設定を指定する方法の基本について概説しました。このセクションでは、XML 構成で単純な宣言型の方法でトランザクションのロールバックを制御する方法について説明します。@Transactional アノテーションを使用して宣言的にロールバックセマンティクスを制御する方法の詳細については、@Transactional 設定を参照してください。

Spring Framework のトランザクションインフラストラクチャに、トランザクションの作業をロールバックすることを示す推奨方法は、トランザクションのコンテキストで現在実行されているコードから Exception をスローすることです。Spring Framework のトランザクションインフラストラクチャコードは、未処理の Exception をキャッチし、コールスタックをバブルアップさせ、トランザクションをロールバック用にマークするかどうかを決定します。

デフォルトの構成では、Spring Framework のトランザクションインフラストラクチャコードは、実行時の非チェック例外の場合にのみ、ロールバックのトランザクションをマークします。つまり、スローされた例外が RuntimeException のインスタンスまたはサブクラスである場合です。(Error インスタンスもデフォルトでロールバックします)。

Spring Framework 5.2 では、デフォルト構成で、Vavr の Try メソッドが 'Failure' を返すときにトランザクションのロールバックをトリガーするサポートも提供されています。これにより、Try を使用して関数型のエラーを処理し、失敗した場合にトランザクションを自動的にロールバックすることができます。Vavr の Try の詳細については、公式の Vavr ドキュメント (英語) を参照してください。以下は、トランザクションメソッドで Vavr の Try を使用する方法の例です。

  • Java

@Transactional
public Try<String> myTransactionalMethod() {
	// If myDataAccessOperation throws an exception, it will be caught by the
	// Try instance created with Try.of() and wrapped inside the Failure class
	// which can be checked using the isFailure() method on the Try instance.
	return Try.of(delegate::myDataAccessOperation);
}

Spring Framework 6.1 では、CompletableFuture (および一般的な Future) の戻り値にも特別な処理があり、元のメソッドから返された時点で例外的に完了していた場合は、そのようなハンドルのロールバックがトリガーされます。これは、実際のメソッド実装が CompletableFuture シグネチャー (実行時に @Async 処理によってプロキシへの呼び出しの実際の非同期ハンドルに自動的に適応される) に準拠する必要がある可能性がある @Async メソッドを対象としており、例外を再スローするのではなく、返されたハンドルで公開することを優先します。

  • Java

@Transactional @Async
public CompletableFuture<String> myTransactionalMethod() {
	try {
		return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
	}
	catch (DataAccessException ex) {
		return CompletableFuture.failedFuture(ex);
	}
}

トランザクションメソッドからスローされたチェック済み例外は、既定の構成ではロールバックされません。ロールバックルールを指定することで、チェック済み例外を含め、どの Exception 型がトランザクションをロールバックするようにマークするかを正確に構成できます。

ロールバックルール

ロールバックルールは、特定の例外がスローされたときにトランザクションをロールバックする必要があるかどうかを決定し、ルールは例外型または例外パターンに基づいています。

ロールバックルールは、rollback-for および no-rollback-for 属性を介して XML で構成できます。これにより、ルールをパターンとして定義できます。@Transactional を使用する場合、ロールバックルールは rollbackFor/noRollbackForrollbackForClassName/noRollbackForClassName 属性を介して構成できます。これにより、ルールをそれぞれ例外型またはパターンに基づいて定義できます。

ロールバックルールが例外型で定義されている場合、その型はスローされた例外の型とそのスーパー型との照合に使用され、型の安全性を提供し、パターンの使用時に発生する可能性のある意図しない一致を回避します。例: jakarta.servlet.ServletException.class の値は、型 jakarta.servlet.ServletException およびそのサブクラスのスローされた例外にのみ一致します。

ロールバックルールが例外パターンで定義されている場合、パターンは完全修飾クラス名または例外型の完全修飾クラス名のサブストリング(Throwable のサブクラスである必要があります)であり、現在ワイルドカードはサポートされていません。例: "jakarta.servlet.ServletException" または "ServletException" の値は、jakarta.servlet.ServletException とそのサブクラスに一致します。

パターンがどの程度具体的であるか、およびパッケージ情報を含めるかどうか(必須ではありません)を慎重に検討する必要があります。例: "Exception" はほぼすべてに一致し、おそらく他のルールを非表示にします。"Exception" が、チェックされたすべての例外のルールを定義することを意図している場合、"java.lang.Exception" は正しいでしょう。"BaseBusinessException" などのより一意の例外名を使用すると、例外パターンに完全修飾クラス名を使用する必要がなくなる可能性があります。

さらに、パターンベースのロールバックルールにより、同様の名前の例外とネストされたクラスが意図せずに一致する可能性があります。これは、スローされた例外の名前にロールバックルール用に構成された例外パターンが含まれている場合、スローされた例外が特定のパターンベースのロールバックルールに一致すると見なされるためです。例: "com.example.CustomException" で一致するように構成されたルールが与えられた場合、そのルールは com.example.CustomExceptionV2 という名前の例外(CustomException と同じパッケージ内の例外ですが、追加のサフィックスが付いています)または com.example.CustomException$AnotherException という名前の例外(CustomException でネストされたクラスとして宣言された例外)と一致します)。

次の XML スニペットは、rollback-for 属性を介して例外パターンを指定することにより、チェックされたアプリケーション固有の Exception 型のロールバックを構成する方法を示しています。

<tx:advice id="txAdvice" transaction-manager="txManager">
	<tx:attributes>
		<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
		<tx:method name="*"/>
	</tx:attributes>
</tx:advice>

例外がスローされたときにトランザクションをロールバックしたくない場合は、「ロールバックなし」ルールを指定することもできます。次の例は、Spring Framework のトランザクションインフラストラクチャに、未処理の InstrumentNotFoundException が発生した場合でもアテンダントトランザクションをコミットするように指示しています。

<tx:advice id="txAdvice">
	<tx:attributes>
		<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
		<tx:method name="*"/>
	</tx:attributes>
</tx:advice>

Spring Framework のトランザクションインフラストラクチャが例外をキャッチし、構成されたロールバックルールを参照して、トランザクションにロールバックのマークを付けるかどうかを判断すると、最も一致するルールが優先されます。次の構成の場合、InstrumentNotFoundException 以外の例外が発生すると、アテンダントトランザクションがロールバックされます。

<tx:advice id="txAdvice">
	<tx:attributes>
		<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
	</tx:attributes>
</tx:advice>

プログラムで必要なロールバックを示すこともできます。このプロセスは単純ですが、非常に侵襲的であり、コードを Spring Framework のトランザクションインフラストラクチャに緊密に結合します。次の例は、必要なロールバックをプログラムで示す方法を示しています。

  • Java

  • Kotlin

public void resolvePosition() {
	try {
		// some business logic...
	} catch (NoProductInStockException ex) {
		// trigger rollback programmatically
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
	}
}
fun resolvePosition() {
	try {
		// some business logic...
	} catch (ex: NoProductInStockException) {
		// trigger rollback programmatically
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
	}
}

可能であれば、宣言的なアプローチを使用してロールバックすることを強くお勧めします。絶対に必要な場合はプログラムによるロールバックを使用できますが、その使用方法は POJO ベースのクリーンなアーキテクチャの実現に直面しています。