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 pytania potrafią występować na rozmowie rekrutacyjnej, 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!