Adnotacja @Transactional w Frameworku Spring
Spring dostarcza potężne mechanizmy do zarządzania transakcjami, a jednym z najczęściej używanych narzędzi jest adnotacja @Transactional
. Dzięki niej możemy łatwo kontrolować sposób wykonywania operacji na bazie danych, zapewniając ich spójność i integralność.
W tym artykule w ramach serii „Szybki strzał„ wyjaśnię, jak działa @Transactional w Springu, jakie ma właściwości i na co zwracać uwagę podczas jej stosowania.
Jak działa @Transactional w Springu?
Adnotacja @Transactional w Springu pochodzi z pakietu org.springframework.transaction.annotation
i pozwala oznaczyć metodę lub klasę jako transakcyjną. Oznacza to, że kod w jej obrębie wykonuje się w ramach jednej transakcji – jeśli coś pójdzie nie tak, cała operacja zostanie wycofana (rollback).
Podstawowe użycie
@Service public class UserService { private final UserRepository userRepository; private final EmailService emailService; public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } @Transactional public User registerUser(UserRequest userRequest) { User user = userRepository.save(userRequest); emailService.sendWelcomeEmail(user); return user; } }
W tym przypadku, jeśli metoda sendWelcomeEmail(user)
rzuci wyjątek, zapis użytkownika do bazy danych zostanie cofnięty.
Propagacja
Atrybut propagation
określa sposób propagowania transakcji w kontekście wywołań metod, co ma kluczowe znaczenie dla zarządzania spójnością i kontrolą nad transakcjami w aplikacji. Wybór odpowiedniego trybu propagacji wpływa na to, jak metody działają względem istniejących transakcji. Dostępne opcje to:
PROPAGATION_REQUIRED
– Domyślna wartość. Jeśli w momencie wywołania metody istnieje aktywna transakcja, zostaje ona użyta. W przeciwnym razie tworzona jest nowa transakcja.PROPAGATION_REQUIRES_NEW
– Zawsze inicjuje nową transakcję, niezależnie od tego, czy inna transakcja już istnieje. Poprzednia transakcja zostaje zawieszona do czasu zakończenia nowej.PROPAGATION_SUPPORTS
– Jeśli istnieje aktywna transakcja, metoda z niej korzysta, ale jeśli transakcja nie jest dostępna, metoda działa bez transakcji.PROPAGATION_NOT_SUPPORTED
– Metoda zawsze wykonuje się poza kontekstem transakcji, co może być użyteczne dla operacji, które nie powinny być częścią transakcji.PROPAGATION_MANDATORY
– Wymaga istnienia aktywnej transakcji; jeśli nie ma żadnej transakcji, zgłaszany jest wyjątek.PROPAGATION_NESTED
– Tworzy transakcję zagnieżdżoną wewnątrz istniejącej transakcji, co pozwala na jej częściowe wycofanie bez wpływu na główną transakcję.
W praktyce dobór odpowiedniego trybu propagacji ma kluczowe znaczenie dla architektury aplikacji. Dobrze zaprojektowany model propagacji transakcji może poprawić wydajność i spójność operacji, a także ułatwić zarządzanie błędami w skomplikowanych procesach biznesowych.
@Transactional(propagation = Propagation.REQUIRES_NEW)
Izolacja
Atrybut isolation
określa poziom izolacji transakcji, co ma kluczowe znaczenie dla kontroli widoczności zmian wprowadzanych w jednej transakcji względem innych. Wybór odpowiedniego poziomu izolacji wpływa zarówno na integralność danych, jak i na wydajność systemu. Dostępne poziomy izolacji to:
ISOLATION_DEFAULT
– Używa domyślnego poziomu izolacji bazy danych, co zazwyczaj zależy od konkretnego silnika bazodanowego.ISOLATION_READ_UNCOMMITTED
– Pozwala na odczyt niezatwierdzonych zmian (tzw. brudnych odczytów), co zwiększa wydajność, ale może prowadzić do niespójności danych.ISOLATION_READ_COMMITTED
– Zapewnia odczyt wyłącznie zatwierdzonych zmian, eliminując problem brudnych odczytów, ale nadal dopuszcza tzw. odczyty niepowtarzalne.ISOLATION_REPEATABLE_READ
– Gwarantuje, że kolejne odczyty tej samej wartości w ramach jednej transakcji zwrócą identyczny wynik, zapobiegając odczytom niepowtarzalnym. Może jednak nadal występować problem zakleszczeń.ISOLATION_SERIALIZABLE
– Najwyższy poziom izolacji, zapewniający pełną separację między transakcjami, eliminując wszelkie anomalie odczytu, ale jednocześnie znacząco wpływający na wydajność systemu.
W praktyce wybór poziomu izolacji powinien być świadomą decyzją – zbyt niski poziom może prowadzić do niespójności danych, a zbyt wysoki do spadku wydajności i problemów z konkurencyjnym dostępem do zasobów.
@Transactional(isolation = Isolation.SERIALIZABLE)
ReadOnly
Atrybut readOnly
(domyślnie false
) w kontekście transakcji informuje system, że dana metoda służy wyłącznie do odczytu danych. Dzięki temu można zoptymalizować wydajność i uniknąć niezamierzonych modyfikacji w bazie danych. Wartość true
pozwala niektórym menedżerom transakcji na zastosowanie dodatkowych optymalizacji, np. pominięcie mechanizmu blokowania czy rezygnację z rejestrowania zmian, co może znacząco wpłynąć na wydajność aplikacji.
W praktyce warto stosować to oznaczenie w metodach pobierających dane – po pierwsze, zwiększa to efektywność operacji na bazie danych, a po drugie, jasno sygnalizuje, że dana transakcja służy wyłącznie do odczytu, co ułatwia zrozumienie i utrzymanie kodu.
@Transactional(readOnly = true)
Manualne zarządzanie transakcją
Oczywiście istnieje możliwość ręcznego zarządzania transakcją w Springu. Przydaje się najczęściej w wyjątkowych sytuacjach, gdzie potrzebujemy bardzo dedykowanego sposobu obsługi transakcji.
@Service public class UserService { private final UserRepository userRepository; private final TransactionTemplate transactionTemplate; public UserService(UserRepository userRepository, PlatformTransactionManager transactionManager) { this.userRepository = userRepository; this.transactionTemplate = new TransactionTemplate(transactionManager); } public User registerUserManually(User user) { return transactionTemplate.execute(status -> { try { return userRepository.save(user); } catch (Exception e) { status.setRollbackOnly(); throw e; } }); } }
O samych szczegółach ręcznego zarządzania transakcją można poczytać w oficjalnej dokumentacji.
Najczęstsze błędy i dobre praktyki
Niepoprawne umieszczanie adnotacji
Adnotacja @Transactional
działa poprawnie tylko na publicznych metodach. Jeśli oznaczysz metodę prywatną lub wywołasz oznaczoną metodę z tej samej klasy, transakcja nie zadziała.
Metoda someMethod()
wywołuje metode registerUser(User user)
z tej samej klasy oznaczoną @Transactional
. W tym przypadku transakcja nie zadziała!
// To nie zadziała !!! class UserService { @Transactional public registerUser(User user) { // kod } public void someMethod() { registerUser(new User()); } }
Dodam, że tego typu przypadku potrafią występować na rozmówię rekrutacyjnie, więc dobrze mieć je przećwiczone 🙂
Obsługa wyjątków
Domyślnie transakcja jest wycofywana tylko dla RuntimeException
i Error
. Jeśli chcesz cofać transakcję także dla checked exceptions
, musisz to jawnie określić:
@Transactional(rollbackFor = Exception.class) public void process() throws Exception { // kod }
Nieoczekiwane propagacje @Transactional w Springu
Podczas zarządzania transakcjami warto zwrócić uwagę nie tylko na to, czy dana metoda jest oznaczona adnotacją @Transactional
, ale także na to, jak zachowa się w przypadku wystąpienia błędu. Kluczowe jest zrozumienie, jak propagacja transakcji wpływa na ich trwałość i możliwość wycofania.
Poniżej znajduje się przykład, w którym w ramach jednej transakcji otwierana jest druga, jednak wyjątek zostaje rzucony tylko w kontekście pierwszej transakcji:
@Transactional public void parentMethod() { firstMethod(); secondMethod(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void firstMethod() { // Nowa transakcja, nie zostanie wycofana razem z parentMethod() } @Transactional public void secondMethod() { throw new RuntimeException(); // Wycofa transakcję parentMethod(), ale nie firstMethod() }
Transakcje w testach jednostkowych
Jeśli piszesz testy w Springu, warto wspomnieć o @Transactional
w testach i użyciu adnotacji @Rollback
. Adnotacja ta pozwala na automatyczne czyszczenie bazy danych po każdym teście.
@SpringBootTest @Transactional @Rollback public class UserServiceTest { @Autowired private UserRepository userRepository; @Test public void shouldSaveUser() { User user = new User("Test User"); userRepository.save(user); assertEquals(1, userRepository.count()); // Dane zostaną usunięte po teście } }
Podsumowanie
Stosowanie @Transactional w Springu jest kluczowe dla zapewnienia integralności danych i poprawnego zarządzania transakcjami. Znajomość jej działania, konfiguracji propagacji oraz obsługi wyjątków pozwala uniknąć wielu pułapek. Warto pamiętać o testowaniu transakcji i unikać typowych błędów, aby nasz kod był bardziej niezawodny i przewidywalny.
public @interface Rollback {
boolean value() default true;
}
…dlatego wystarczy pisać @Rollback / nie trzeba pisać @Rollback(true)
Racja, niepotrzebne jest to true.
Dzięki za wyłapanie!