Spring で MyBatis(MySQL) つかって ネストした トランザクション制御しようとして以下エラーに遭遇した
Caused by: org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
トランザクションの伝播については、PROPAGATION_REQUIRED
の理解が必要
とのことで調べた
要は、明示的に Transaction
を貼りたい場合にどういった振る舞いをすべきかを指定する必要があり、
選択肢は以下の2つある
PROPAGATION_REQUIRED
- 存在しない場合は新たに作成、存在する場合は参加
- 内部スコープがロールバック専用マーカーを設定した場合、外部トランザクションはこれを予期していないため、 予期しないロールバックが発生するとUnexpectedRollbackExceptionがスローされる
PROPAGATION_REQUIRES_NEW
- 常に独立したトランザクションを開始
- 独立してコミットまたはロールバック可能
以下詳細
PROPAGATION_REQUIREDの理解 tx prop required PROPAGATION_REQUIREDは物理的なトランザクションを強制します。既存のトランザクションがまだ存在しない場合は現在のスコープでローカルに、または大きなスコープで定義された既存の’外部’トランザクションに参加します。これは同一スレッド内の一般的なコールスタックの配置(例えば、すべての基礎となるリソースがサービスレベルのトランザクションに参加しなければならない、いくつかのリポジトリメソッドに委任するサービスファサードなど)では良いデフォルトです。
デフォルトでは、参加するトランザクションは外部スコープの特性に参加し、ローカルの分離レベル、タイムアウト値、または読み取り専用フラグ(あれば)を黙って無視します。異なる分離レベルで既存のトランザクションに参加する際に分離レベルの宣言を拒否する場合は、トランザクションマネージャーでvalidateExistingTransactionsフラグをtrueに切り替えることを検討してください。この非寛容モードは、読み取り専用の不一致(つまり、読み取り専用の外部スコープに参加しようとする内部の読み書き可能なトランザクション)も拒否します。 伝播設定がPROPAGATION_REQUIREDの場合、その設定が適用される各メソッドに対して論理的なトランザクションスコープが作成されます。各論理的なトランザクションスコープは、ロールバックのみのステータスを個々に決定でき、外部のトランザクションスコープは内部のトランザクションスコープから論理的に独立しています。標準的なPROPAGATION_REQUIREDの動作の場合、これらすべてのスコープは同一の物理的なトランザクションにマップされます。したがって、内部のトランザクションスコープで設定されたロールバックのみのマーカーは、外部のトランザクションが実際にコミットする可能性に影響を与えます。
しかし、内部のトランザクショクションスコープがロールバック専用マーカーを設定した場合、外部のトランザクションは自身でロールバックを決定していないため、内部のトランザクションスコープによって黙ってトリガーされるロールバックは予期しないものです。その時点で対応するUnexpectedRollbackExceptionがスローされます。これはトランザクションの呼び出し元がコミットが実行されたと誤って推測することがないようにするための期待される挙動です。したがって、内部のトランザクション(外部の呼び出し元が認識していない)が黙ってトランザクションをロールバック専用とマークすると、外部の呼び出し元はまだコミットを呼び出します。外部の呼び出し元は、ロールバックが実行されたことを明確に示すために、UnexpectedRollbackExceptionを受け取る必要があります。
PROPAGATION_REQUIRES_NEWの理解 tx prop requires new PROPAGATION_REQUIRES_NEWは、対象となるトランザクションスコープごとに常に独立した物理的なトランザクションを使用し、外部のスコープの既存のトランザクションには決して参加しない、という点でPROPAGATION_REQUIREDとは対照的です。このような配置では、基礎となるリソーストランザクションは異なり、したがって、独立してコミットまたはロールバックでき、外部のトランザクションは内部のトランザクションのロールバック状態に影響されず、内部のトランザクションのロックはその完了直後にすぐに解放されます。このような独立した内部トランザクションは、自身の分離レベル、タイムアウト、読み取り専用設定を宣言し、外部のトランザクションの特性を継承しないこともできます。
以下翻訳元
Understanding PROPAGATION_REQUIRED tx prop required PROPAGATION_REQUIRED enforces a physical transaction, either locally for the current scope if no transaction exists yet or participating in an existing ‘outer’ transaction defined for a larger scope. This is a fine default in common call stack arrangements within the same thread (for example, a service facade that delegates to several repository methods where all the underlying resources have to participate in the service-level transaction).
By default, a participating transaction joins the characteristics of the outer scope, silently ignoring the local isolation level, timeout value, or read-only flag (if any). Consider switching the validateExistingTransactions flag to true on your transaction manager if you want isolation level declarations to be rejected when participating in an existing transaction with a different isolation level. This non-lenient mode also rejects read-only mismatches (that is, an inner read-write transaction that tries to participate in a read-only outer scope). When the propagation setting is PROPAGATION_REQUIRED, a logical transaction scope is created for each method upon which the setting is applied. Each such logical transaction scope can determine rollback-only status individually, with an outer transaction scope being logically independent from the inner transaction scope. In the case of standard PROPAGATION_REQUIRED behavior, all these scopes are mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transaction’s chance to actually commit.
However, in the case where an inner transaction scope sets the rollback-only marker, the outer transaction has not decided on the rollback itself, so the rollback (silently triggered by the inner transaction scope) is unexpected. A corresponding UnexpectedRollbackException is thrown at that point. This is expected behavior so that the caller of a transaction can never be misled to assume that a commit was performed when it really was not. So, if an inner transaction (of which the outer caller is not aware) silently marks a transaction as rollback-only, the outer caller still calls commit. The outer caller needs to receive an UnexpectedRollbackException to indicate clearly that a rollback was performed instead.
Understanding PROPAGATION_REQUIRES_NEW tx prop requires new PROPAGATION_REQUIRES_NEW, in contrast to PROPAGATION_REQUIRED, always uses an independent physical transaction for each affected transaction scope, never participating in an existing transaction for an outer scope. In such an arrangement, the underlying resource transactions are different and, hence, can commit or roll back independently, with an outer transaction not affected by an inner transaction’s rollback status and with an inner transaction’s locks released immediately after its completion. Such an independent inner transaction can also declare its own isolation level, timeout, and read-only settings and not inherit an outer transaction’s characteristics.
最終的な実装は以下
TransactionDefinition def = new DefaultTransactionDefinition(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = platformTransactionManager.getTransaction(def);
if (commit) {
platformTransactionManager.commit(status);
return;
}
platformTransactionManager.rollback(status);