トランザクション管理下で、内部的に例外が発生¶
このエラー:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
は、Springのトランザクション管理下で、内部的に例外が発生し、トランザクションが「ロールバック専用」にマークされたために、正常終了しようとした時にロールバックされた、という状況を示します。
🔍 原因の典型パターン¶
-
内部メソッドで
RuntimeExceptionが発生し、catchされたが再スローされなかった- Springは例外を捕捉してもトランザクションはrollback-onlyにマークされる。
- その後、呼び出し元では例外が発生していないように見えても、トランザクションの整合性を保つために最終的にロールバックされる。
-
手動で
TransactionStatus#setRollbackOnly()を呼んだ -
@Transactionalの属性ミスマッチ- 呼び出し先が
@Transactional(propagation = Propagation.REQUIRES_NEW)で強制的に新しいトランザクションを開始し、内部でロールバック→親には例外を投げないが、整合性が崩れて rollback-only 扱いになる。
- 呼び出し先が
-
例外が変換された (
@ControllerAdvice,@ExceptionHandler,try-catchなど)- DBエラーなどが
DataIntegrityViolationExceptionなどにラップされて catch → ロールバックマークされたまま終了。
- DBエラーなどが
→正しい動作です!!!
内部で例外が発生してもトランザクションをロールバックせずにコミットしたい¶
Springのトランザクションにおいて「内部で例外が発生してもトランザクションをロールバックせずにコミットしたい」という要件は 原則的には「例外をトランザクション管理に伝播させない」ことで実現できます。ただし、細心の注意が必要です。
✅ 方法1:try-catch して例外を握りつぶす(※一番単純)¶
@Transactional
public void outerService() {
try {
innerService.doSomething(); // ← RuntimeExceptionが出てもキャッチ
} catch (Exception e) {
log.warn("処理中にエラーが発生しましたが、無視します: " + e.getMessage());
// rollbackOnlyにマークされていないことが重要
}
// ここで他の処理を続けてもトランザクションはCOMMITされる
}
⚠️注意¶
-
innerService.doSomething()が@Transactional付きで、自分で例外を throw して rollback していた場合、catch しても rollback-only 状態になっていることがあります。 - その場合、catch しても
outerService()側がUnexpectedRollbackExceptionで落ちます。
✅ 方法2:@Transactional(noRollbackFor = ...) を使う¶
内部の例外を catch せずに伝播させたいけど、ロールバックはしたくない場合:
@Transactional(noRollbackFor = { MyException.class })
public void outerService() {
innerService.doSomething(); // MyExceptionがスローされてもロールバックされない
}
public void doSomething() {
throw new MyException("これは無視していい例外です");
}
✅ 方法3:内部処理を別トランザクションに切り出す(失敗しても親トランザクションに影響を与えない)¶
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional
public void outerService() {
try {
innerService.innerTransactionalLogic(); // 新しいトランザクションで処理
} catch (Exception e) {
log.warn("内部処理失敗、でも親トランザクションは続行", e);
}
// 親トランザクションの他の処理
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerTransactionalLogic() {
// ここで例外が起きても親トランザクションには影響なし
throw new RuntimeException("失敗したよ");
}
}
✅ 効果¶
- 内部処理の失敗は独立してロールバック
- 親のトランザクションは正常にコミットできる
✅ よくある罠¶
| ケース | 現象 | 対策 |
|---|---|---|
例外をcatchしたけど @Transactional の中 |
rollbackOnlyにマークされていて UnexpectedRollbackException
|
catchした後に TransactionAspectSupport.currentTransactionStatus().isRollbackOnly() を確認する |
内部で @Transactional を使い、RuntimeExceptionをthrow |
親トランザクションが巻き込まれて rollback |
Propagation.REQUIRES_NEW を使って独立させる |
noRollbackFor を忘れた |
指定例外でロールバックされてしまう | 必要な例外を明示的に設定する |
✅ 結論:どうすればよいか?¶
内部例外を無視してコミットしたいなら、次のどれか:
| 方法 | 状況 |
|---|---|
try-catch して握りつぶす |
自然で簡単。rollbackOnlyマークされてない前提 |
@Transactional(noRollbackFor=...) |
明示的にこの例外ではロールバックしないようにしたい場合 |
@Transactional(propagation = REQUIRES_NEW) |
内部の失敗を親に影響させたくない場合 |
具体的なコードがあれば、それに合わせてアドバイスもできますので、お気軽に貼ってください。