Co to jest i jak działa @Transactional w Springu?

uprogramisty.pl 1 tydzień temu

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 adekwatnoś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, iż kod w jej obrębie wykonuje się w ramach jednej transakcji – jeżeli 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żeli 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 najważniejsze 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żeli 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żeli istnieje aktywna transakcja, metoda z niej korzysta, ale jeżeli 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żeli 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 najważniejsze 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 najważniejsze 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 zwykle 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 przez cały czas dopuszcza tzw. odczyty niepowtarzalne.
  • ISOLATION_REPEATABLE_READ – Gwarantuje, iż kolejne odczyty tej samej wartości w ramach jednej transakcji zwrócą identyczny wynik, zapobiegając odczytom niepowtarzalnym. Może jednak przez cały czas 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, iż 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, iż 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ść manualnego 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 manualnego 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żeli 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, iż 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żeli 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. najważniejsze 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 @Rollback(true). Adnotacja ta pozwala na automatyczne czyszczenie bazy danych po każdym teście.

@SpringBootTest @Transactional @Rollback(true) 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 najważniejsze 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.

Idź do oryginalnego materiału