Spring @Transactional pułapki

private

Żadna metoda prywatna oznaczona adnotacją @Transactional nie będzie wykonana w ramach transakcji (chyba, że metoda ją wołająca już była w jakiejś). Na pewno adnotacja @Transactional nie zadziała na takiej metodzie.

try-catch

Jeśli metoda A oznaczona @Transactional wyłapuje wszystkie wyjątki rzucane przez metodę B (także oznaczoną @Transactional), to gdy metoda B rzuci wyjątek, dostaniemy UnexpectedRollbackException bez śladu oryginalnego wyjątku.

Dokładniejsze wyjaśnienie:

Załóżmy, że mamy repozytorium, które może czasami rzucić wyjątek (w naszym przypadku robi to zawsze). I metoda repozytorium jest oznaczona jak @Transactional:

@Repository
public class SomeRepository {

    @Transactional
    public void throwRuntimeException(){
        throw new RuntimeException();
    }

}

I mamy także serwis, który wywołuje metodę repozytorium i chce przechwytywać te wyjątki żeby później je jakoś obsługiwać:

@Service
public class SomeService {
    
    @Autowired
    private SomeRepository someRepository;

    @Transactional
    public void catchRuntimeException() {
        try {
            repository.throwRuntimeException();
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }
}

Niestety takie łapanie wyjątków powoduje drobne problemy. Wyjątek zostanie rzeczywiście przechwycony w bloku try-catch. Ale z powodu propagacji transakcji (z serwisu na repozytorium) cała transakcja (rozpoczęta w serwisie) jest już oznaczona jako rollbackOnly (jest przeznaczona do cycofania), co spowoduje w rezultacie rzucenie wyjątku UnexpectedRollbackException w momencie gdy przebieg programu dotrze do momentu, w którym kończy się kod wykonywany w ramach transakcji (w naszym przypadku będzie to koniec metody SomeService.catchRuntimeException).

Spring w momencie uruchamiania metody repozytorium opakowuje to wywołanie swoimi klasami i jeśli metoda rzucająca wyjątek nie obsłuży wyjątku w ramach tej metody (tylko zostanie on przekazany do metody wyżej), to klasa opakowująca oznacza transakcję jako rollbackOnly.

Po więcej przykładów zapraszam TransactionalTest.java .