プロジェクト

全般

プロフィール

操作

トランザクション管理下で、内部的に例外が発生

このエラー:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

は、Springのトランザクション管理下で、内部的に例外が発生し、トランザクションが「ロールバック専用」にマークされたために、正常終了しようとした時にロールバックされた、という状況を示します。


🔍 原因の典型パターン

  1. 内部メソッドでRuntimeExceptionが発生し、catchされたが再スローされなかった

    • Springは例外を捕捉してもトランザクションはrollback-onlyにマークされる。
    • その後、呼び出し元では例外が発生していないように見えても、トランザクションの整合性を保つために最終的にロールバックされる。
  2. 手動で TransactionStatus#setRollbackOnly() を呼んだ

  3. @Transactional の属性ミスマッチ

    • 呼び出し先が @Transactional(propagation = Propagation.REQUIRES_NEW) で強制的に新しいトランザクションを開始し、内部でロールバック→親には例外を投げないが、整合性が崩れて rollback-only 扱いになる。
  4. 例外が変換された (@ControllerAdvice, @ExceptionHandler, try-catch など)

    • DBエラーなどが DataIntegrityViolationException などにラップされて catch → ロールバックマークされたまま終了。

→正しい動作です!!!

内部で例外が発生してもトランザクションをロールバックせずにコミットしたい

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) 内部の失敗を親に影響させたくない場合

具体的なコードがあれば、それに合わせてアドバイスもできますので、お気軽に貼ってください。

渡邉 拓英 さんが1年以上前に更新 · 0件の履歴