Transakcje odgrywają istotną rolę w aplikacjach korzystających z relacyjnych baz danych, zapewniając spójność wykonywanych operacji. Spring Framework znacząco upraszcza zarządzanie nimi, udostępniając mechanizm deklaratywny oparty na adnotacji @Transactional.
Dowiesz się, jak działa @Transactional, w jaki sposób Spring wykorzystuje mechanizm AOP do zarządzania transakcjami oraz czym różnią się poszczególne poziomy propagacji. Omówię również konfigurację izolacji transakcji, mechanizm rollback oraz najczęstsze problemy i dobre praktyki.
Czym są transakcje?
Transakcja to zbiór operacji na bazie danych, które są traktowane jako jedna logiczna całość. Oznacza to, że wszystkie operacje muszą zakończyć się sukcesem albo żadna z nich nie zostanie trwale zapisana.
Przykładem może być przelew bankowy, który wymaga pobrania środków z jednego konta i dodania ich do drugiego. Jeśli druga operacja zakończy się błędem, pierwsza powinna zostać wycofana. Transakcje opierają się na właściwościach ACID (Atomicity, Consistency, Isolation, Durability), które zapewniają spójność i niezawodność operacji na danych.
Więcej na ten temat napisałem w osobnym artykule o transakcjach, ACID i poziomach izolacji w SQL, do którego przeczytania Cię zachęcam.
W Spring Framework zarządzanie transakcjami jest realizowane m.in. za pomocą adnotacji @Transactional, której działanie omówię w kolejnych rozdziałach.
Zarządzanie transakcjami w Spring Framework
Spring Framework udostępnia mechanizmy umożliwiające zarządzanie transakcjami bez konieczności ręcznego otwierania, zatwierdzania i wycofywania ich za pomocą JDBC.
Podstawą tego mechanizmu jest interfejs PlatformTransactionManager, który odpowiada za zarządzanie cyklem życia transakcji. Udostępnia trzy podstawowe metody:
getTransaction()– pobiera istniejącą transakcję lub rozpoczyna nową, zależnie od konfiguracji.commit()– zatwierdza transakcję.rollback()– wycofuje transakcję.
Spring dostarcza różne implementacje tego interfejsu, m.in. DataSourceTransactionManager do obsługi transakcji JDBC oraz JpaTransactionManager do współpracy z JPA.
Transakcje deklaratywne i programistyczne
W Spring Framework można zarządzać transakcjami na dwa sposoby:
Zarządzanie deklaratywne – polega na wykorzystaniu adnotacji @Transactional. Programista określa, które metody mają być wykonywane w ramach transakcji, natomiast Spring automatycznie zarządza jej rozpoczęciem, zatwierdzeniem lub wycofaniem.
Zarządzanie programistyczne – polega na bezpośrednim sterowaniu transakcją w kodzie, np. za pomocą TransactionTemplate lub PlatformTransactionManager. Zapewnia większą kontrolę nad jej przebiegiem, ale wymaga dodatkowego kodu.
W większości typowych aplikacji Spring Boot wykorzystywane jest podejście deklaratywne.
Jak Spring zarządza transakcjami?
Deklaratywne zarządzanie transakcjami opiera się domyślnie na mechanizmie Spring AOP i wzorcu Proxy.
Gdy inny komponent wywołuje metodę oznaczoną adnotacją @Transactional, wywołanie przechodzi przez obiekt proxy, który przy użyciu TransactionInterceptor i odpowiedniego menedżera transakcji:
- Rozpoczyna nową transakcję lub dołącza do istniejącej.
- Wywołuje właściwą metodę biznesową.
- Zatwierdza transakcję, jeśli operacja zakończyła się poprawnie.
- Wycofuje transakcję, jeśli wystąpił wyjątek kwalifikujący się do rollbacku.
Dzięki temu logika biznesowa pozostaje oddzielona od mechanizmu zarządzania transakcjami (za pomocą mechanizmu o nazwie proxy).
Przykład użycia @Transactional
Załóżmy, że mamy prostą aplikację umożliwiającą zarządzanie użytkownikami. Do komunikacji z bazą danych wykorzystamy Hibernate oraz SessionFactory, które zostały omówione w artykule o frameworku Hibernate.
@Service
public class UserService {
private final SessionFactory sessionFactory;
public UserService(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Transactional(readOnly = true)
public User getUser(Long id) {
return sessionFactory.getCurrentSession()
.find(User.class, id);
}
@Transactional
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
}
@Transactional
public void updateUser(User user) {
sessionFactory.getCurrentSession().merge(user);
}
@Transactional
public void deleteUser(Long id) {
User user = sessionFactory.getCurrentSession()
.find(User.class, id);
if (user != null) {
sessionFactory.getCurrentSession().remove(user);
}
}
}W powyższym przykładzie każda metoda została oznaczona adnotacją @Transactional, dzięki czemu Spring automatycznie zarządza transakcją podczas jej wykonywania.
Warto zwrócić uwagę na parametr readOnly = true zastosowany w metodzie getUser(). Informuje on mechanizm zarządzania transakcjami, że metoda służy wyłącznie do odczytu danych, co w zależności od używanej technologii może umożliwić dodatkowe optymalizacje.
Pozostałe metody wykorzystują domyślną konfigurację @Transactional, ponieważ odpowiadają za modyfikowanie danych w bazie.
W przykładzie zakładam, że SessionFactory jest skonfigurowany do współpracy ze Springowym menedżerem transakcji.
Możez też dodawać @Transactional na samej klasie (wtedy zadziała to tak, jakby została ona nadana na każdej metodzie)
Jak działa @Transactional?
Mechanizm działania @Transactional opiera się na programowaniu aspektowym (Spring AOP) oraz wzorcu projektowym Proxy, które zostały omówione w poprzednim artykule o Spring Framework.
W tradycyjnym podejściu programista musi samodzielnie rozpocząć transakcję przed wykonaniem operacji na bazie danych, a następnie ją zatwierdzić lub wycofać. Dzięki @Transactional cała ta logika jest realizowana automatycznie przez Spring.
Gdy Spring wykryje klasę z metodami oznaczonymi adnotacją @Transactional, tworzy dla niej obiekt proxy, który przechwytuje wywołania tych metod. Proxy otacza właściwy kod dodatkowymi operacjami związanymi z zarządzaniem transakcją.
W uproszczeniu mechanizm ten wygląda następująco:
// Rozpoczęcie transakcji transaction.begin(); // Wykonanie właściwej metody userService.createUser(user); // Zatwierdzenie transakcji transaction.commit();
Powyższy kod jest jedynie ilustracją działania mechanizmu, a nie rzeczywistą implementacją Springa. W praktyce wszystkie te czynności wykonuje odpowiedni menedżer transakcji, wykorzystywany przez mechanizm AOP.
W efekcie programista może skupić się na logice biznesowej, a Spring odpowiada za zarządzanie cyklem życia transakcji.
Rollback – wycofywanie transakcji
Rollback to mechanizm umożliwiający wycofanie zmian wykonanych w ramach transakcji. W Spring Framework jest on obsługiwany automatycznie przez @Transactional, jednak jego zachowanie zależy od rodzaju zgłoszonego wyjątku.
RuntimeException a checked exceptions
Domyślnie Spring wykonuje rollback w przypadku wystąpienia:
- RuntimeException – wyjątków niekontrolowanych (unchecked exceptions), np.
IllegalArgumentException,NullPointerException. - Error – poważnych błędów środowiska uruchomieniowego, np.
OutOfMemoryError.
Natomiast wyjątki kontrolowane (checked exceptions), takie jak IOException czy SQLException, domyślnie nie powodują wycofania transakcji.
Rozważmy poniższy przykład:
@Transactional
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
throw new RuntimeException("Wystąpił błąd");
}Po zgłoszeniu wyjątku RuntimeException Spring automatycznie wycofa transakcję. Oznacza to, że użytkownik nie zostanie trwale zapisany w bazie danych.
Parametry rollbackFor i noRollbackFor
Spring umożliwia zmianę domyślnego zachowania za pomocą dwóch parametrów adnotacji @Transactional:
rollbackFor – określa dodatkowe rodzaje wyjątków, które powinny powodować wycofanie transakcji.
@Transactional(rollbackFor = IOException.class)
public void createUser(User user) throws IOException {
// ...
}W tym przypadku transakcja zostanie wycofana również wtedy, gdy metoda zakończy się wyjątkiem IOException.
noRollbackFor – określa wyjątki, które nie powinny powodować automatycznego wycofania transakcji.
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void createUser(User user) {
// ...
}W tym przypadku wystąpienie IllegalArgumentException nie spowoduje automatycznego rollbacku. Spring spróbuje zatwierdzić transakcję, mimo że metoda zakończyła się wyjątkiem.
Co się stanie, gdy przechwycimy wyjątek?
Warto pamiętać, że Spring podejmuje decyzję o rollbacku na podstawie wyjątku, który dotrze do mechanizmu zarządzania transakcjami.
@Transactional
public void createUser(User user) {
try {
sessionFactory.getCurrentSession().persist(user);
throw new RuntimeException("Wystąpił błąd");
} catch (RuntimeException e) {
System.out.println("Błąd został przechwycony");
}
}W powyższym przykładzie wyjątek zostaje przechwycony wewnątrz metody. Ponieważ nie jest przekazywany dalej, Spring nie otrzymuje sygnału do automatycznego wycofania transakcji na podstawie tego wyjątku.
Ważne: nie oznacza to, że każda transakcja z przechwyconym wyjątkiem zostanie zatwierdzona. Jeśli transakcja została wcześniej oznaczona jako wymagająca wycofania (rollback-only), nadal może zostać wycofana.
Dlatego nie należy bezrefleksyjnie przechwytywać wyjątków wewnątrz metod oznaczonych @Transactional, szczególnie jeśli wcześniejsze operacje na bazie danych mogły zakończyć się błędem.
Propagacje transakcji
Częstym problemem w dużych aplikacjach jest zagnieżdżanie jednej transakcji w drugije. Jak wtedy aplikacja powinna się zachować, gdy jedna z nich zglosi wyjątek? W którym momencie transakcja powinna zostać cofnięta a w którym nie? Aby to zrozumieć, spójrz na poniższy przyklad.
Propagacja REQUIRED (domyślna)
Zakładam, że w aplikacji są dwa serwisy: UserService, który odpowiada za tworzenie użytkowników, oraz AuditService, który zapisuje informacje o wykonanych operacjach.
@Service
public class UserService {
private final SessionFactory sessionFactory;
private final AuditService auditService;
public UserService(SessionFactory sessionFactory,
AuditService auditService) {
this.sessionFactory = sessionFactory;
this.auditService = auditService;
}
@Transactional
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
auditService.saveAudit("User created");
}
}Drugi serwis również wykorzystuje adnotację @Transactional:
@Service
public class AuditService {
private final SessionFactory sessionFactory;
public AuditService(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Transactional
public void saveAudit(String message) {
AuditLog audit = new AuditLog(message);
sessionFactory.getCurrentSession().persist(audit);
}
}W powyższym przykładzie zarówno createUser(), jak i saveAudit() zostały oznaczone adnotacją @Transactional. Ponieważ domyślnym poziomem propagacji jest Propagation.REQUIRED, obie metody uczestniczą w tej samej transakcji.
Przebieg wykonania wygląda następująco:
- Wywołanie
createUser()powoduje rozpoczęcie transakcji. - Hibernate zapisuje nowego użytkownika w ramach tej transakcji.
- Metoda
createUser()wywołujeauditService.saveAudit(). - Spring wykrywa istniejącą transakcję, dlatego
saveAudit()dołącza do niej zamiast rozpoczynać nową. - Po zakończeniu
saveAudit()sterowanie wraca docreateUser(). - Po poprawnym zakończeniu
createUser()Spring zatwierdza całą transakcję.
W efekcie zapis użytkownika oraz informacji audytowej stanowią jedną operację transakcyjną. Jeśli transakcja zostanie wycofana, zmiany w obu tabelach również zostaną wycofane.</escape>
Warto zwrócić uwagę, że zakończenie metody saveAudit() nie powoduje zatwierdzenia transakcji. Ponieważ metoda dołączyła do istniejącej transakcji, jej ostateczne zatwierdzenie następuje dopiero po zakończeniu zewnętrznej metody createUser().
Propagacja REQUIRES_NEW
REQUIRES_NEW wymusza rozpoczęcie nowej, niezależnej transakcji.
- Jeśli istnieje aktywna transakcja, zostaje ona tymczasowo zawieszona.
- Spring rozpoczyna nową transakcję dla wywoływanej metody.
- Po zakończeniu nowej transakcji poprzednia zostaje wznowiona.
Najważniejszą różnicą względem REQUIRED jest to, że obie transakcje mogą zostać zatwierdzone lub wycofane niezależnie od siebie.
Przykład:
Załóżmy, że podczas tworzenia użytkownika chcemy również zapisać informację o wykonanej operacji.
@Service
public class UserService {
private final SessionFactory sessionFactory;
private final AuditService auditService;
// Konstruktor
@Transactional
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
auditService.saveAudit();
}
}Drugi serwis odpowiada za zapisanie informacji audytowej:
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit() {
// Zapis informacji o operacji w bazie danych
}
}W tym przypadku:
createUser()rozpoczyna transakcję A.saveAudit()zawiesza transakcję A i rozpoczyna niezależną transakcję B.- Po zakończeniu
saveAudit()transakcja B zostaje zatwierdzona. - Spring wznawia transakcję A.
Jeżeli później transakcja A zostanie wycofana, zapis audytowy wykonany w zatwierdzonej transakcji B pozostanie w bazie danych.
Gdyby saveAudit() korzystała z domyślnej propagacji REQUIRED, obie metody uczestniczyłyby w tej samej transakcji, a jej wycofanie obejmowałoby również zapis audytowy.
Propagation.SUPPORTS
SUPPORTS oznacza, że metoda może uczestniczyć w istniejącej transakcji, ale jej nie wymaga. Jeśli transakcja istnieje, metoda dołącza do niej. W przeciwnym przypadku wykonuje się bez transakcji.
Dobrym przykładem jest użycie operacji Hibernate, która korzysta z polecenia SQL Select. Przy odpowiednim konfiguracji sesji Hibernate powinien ją wykonać nawet, jeśli zostanie zostanie użyta w metodzie, która nie otwiera transakcji (bo domyślnie odczyt w bazie może się odbywać bez transakcji).
@Transactional(propagation = Propagation.SUPPORTS)
public User getUser(Long id) {
// ...
}Propagation.MANDATORY
Załóżmy taką zmianę w moim kodzie:
public void createUser(User user) {
auditService.saveAudit("User created");
sessionFactory.getCurrentSession().persist(user);
}Natomiast w AuditService:
@Transactional(propagation = Propagation.MANDATORY)
public void saveAudit(String message) {
// ...
}MANDATORY wymaga istnienia aktywnej transakcji. Jeśli metoda zostanie wywołana bez aktywnej transakcji, Spring zgłosi wyjątek IllegalTransactionStateException.
W tej wersji kodu, saveAudit został oznaczony jako MANDATORY, a z createUser usunięto @Transactional. W efekcie podczas użycia tej metody zostanie wyrzuci wyjątek.
Propagation.NOT_SUPPORTED
NOT_SUPPORTED oznacza, że metoda powinna zostać wykonana poza transakcją.
Jeżeli istnieje aktywna transakcja, Spring zawiesza ją na czas wykonania metody, a następnie wznawia.
W pierwotnym przykładzie zastosowanie NOT_SUPPORTED w metodzie saveAudit() spowoduje zawieszenie transakcji rozpoczętej przez createUser(). Metoda saveAudit() zostanie wykonana bez aktywnej transakcji.
Ponieważ wykorzystywany jest tu Hibernate do zapisywania danych, takie rozwiązanie może prowadzić do błędów lub braku trwałego zapisu zmian. Dlatego NOT_SUPPORTED nie jest odpowiednim wyborem dla tej operacji.
Propagation.NEVER
NEVER wymusza wykonywanie metody bez aktywnej transakcji.
W przeciwieństwie do NOT_SUPPORTED, Spring nie zawiesza istniejącej transakcji, lecz zgłasza wyjątek IllegalTransactionStateException. W moim przykładzie, oznacza to, że jeśli ta propagacja zostanie narzucona na metodę saveAudit to wyrzuci ona wyjątek z racji, że została wywołana przez createUser, która ma już otwartą transakcję.
Propagation.NESTED
Na koniec zostawiłem propagację NESTED, która umożliwia wykonanie operacji w ramach transakcji zagnieżdżonej. Jest to na tyle specyficzny przypadek, że warto poświęcić mu trochę więcej czasu.
NESTED oznacza, że jeśli istnieje aktywna transakcja, Spring może utworzyć punkt zapisu (savepoint), do którego można wycofać zmiany wykonane przez wewnętrzną operację, bez konieczności wycofywania całej transakcji.
Jeśli transakcja nie istnieje, zachowanie jest takie samo jak w przypadku REQUIRED.
W przeciwieństwie do REQUIRES_NEW, NESTED nie tworzy niezależnej transakcji fizycznej. Wykorzystuje istniejącą transakcję i mechanizm savepointów.
W przypakładzie z UserService oznacza to, że jeśli saveAudit zostanie oznaczone jako NESTED, to operacja ta będzie dołączana do tej samej transakcji co createUser (tak jak w domyślnej wersji, czyli z REQUIRED). Niestety Hibernate domyślnie nie obsługuje zagnieżdżonych transakcji, więc nie będzie wstanie zrobić savepoint i wyrzuci wyjątek NestedTransactionNotSupportedException.
Aby włączyć obsługę savepointów przy korzystaniu z Hibernate, musisz zmienić konfigurację HibernateTransactionManager, ustawiając nestedTransactionAllowed = true.
Podsumowanie poziomów propagacji
| Propagacja | Istnieje transakcja | Brak transakcji |
|---|---|---|
REQUIRED | Dołącza | Tworzy nową |
REQUIRES_NEW | Zawiesza i tworzy nową | Tworzy nową |
NESTED | Tworzy zakres zagnieżdżony* | Tworzy nową |
SUPPORTS | Dołącza | Działa bez transakcji |
MANDATORY | Dołącza | Zgłasza wyjątek |
NOT_SUPPORTED | Zawiesza | Działa bez transakcji |
NEVER | Zgłasza wyjątek | Działa bez transakcji |
* Przy wsparciu odpowiedniego menedżera transakcji i mechanizmu savepointów.
W praktyce najczęściej spotykanymi poziomami propagacji są REQUIRED i REQUIRES_NEW. Pierwszy pozwala grupować operacje w jednej transakcji, natomiast drugi umożliwia wykonywanie niezależnych operacji transakcyjnych.
Warto jednak pamiętać, że REQUIRES_NEW może wymagać dodatkowego połączenia z bazą danych, dlatego jego nadużywanie może prowadzić do wyczerpania puli połączeń.
Poziomy izolacji transakcji (Isolation)
Poziom izolacji transakcji określa, w jaki sposób równolegle wykonywane transakcje mogą wpływać na siebie nawzajem. Pozwala kontrolować występowanie takich problemów jak Dirty Read, Non-repeatable Read czy Phantom Read.
W Spring Framework poziom izolacji można skonfigurować za pomocą parametru isolation adnotacji @Transactional.
Przykładowo:
@Transactional(
isolation = Isolation.READ_COMMITTED,
readOnly = true
)
public User getUser(Long id) {
return sessionFactory.getCurrentSession()
.find(User.class, id);
}W powyższym przykładzie zastosowano poziom READ_COMMITTED, który zapobiega odczytywaniu niezatwierdzonych zmian wprowadzonych przez inne transakcje.
Spring udostępnia następujące poziomy izolacji:
DEFAULT– wykorzystuje domyślny poziom izolacji bazy danych (z reguły większość baz używa domyślnieREAD_COMMITTED).READ_UNCOMMITTED– dopuszcza odczyt niezatwierdzonych danych.READ_COMMITTED– zapobiega odczytowi niezatwierdzonych danych.REPEATABLE_READ– dodatkowo zapewnia powtarzalność odczytu tych samych wierszy w ramach transakcji.SERIALIZABLE– najbardziej restrykcyjny poziom, zapewniający efekt równoważny szeregowemu wykonywaniu transakcji.
Domyślnie Spring wykorzystuje Isolation.DEFAULT, co oznacza, że o poziomie izolacji decyduje baza danych.
Warto pamiętać, że parametr isolation ma zastosowanie podczas rozpoczynania nowej transakcji. Jeśli metoda z propagacją REQUIRED dołączy do już istniejącej transakcji, domyślnie przejmie jej poziom izolacji, nawet jeśli w adnotacji określono inny.
Dostępność i rzeczywiste działanie poszczególnych poziomów izolacji zależą również od używanej bazy danych.
Pozostałe parametry @Transactional
Poza propagacją i poziomem izolacji Spring Framework umożliwia konfigurację dodatkowych parametrów transakcji. Warto zwrócić uwagę przede wszystkim na readOnly, timeout oraz transactionManager.
readOnly
Parametr readOnly informuje Spring, że transakcja służy wyłącznie do odczytu danych.
@Transactional(readOnly = true)
public User getUser(Long id) {
return sessionFactory.getCurrentSession()
.find(User.class, id);
}Domyślnie readOnly ma wartość false.
Ustawienie readOnly = true może poprawić wydajność operacji odczytu. W przypadku Hibernate Spring może odpowiednio skonfigurować sesję, ograniczając mechanizmy związane z wykrywaniem zmian w encjach.
Należy jednak pamiętać, że readOnly = true nie gwarantuje zablokowania operacji zapisu. Jest przede wszystkim wskazówką optymalizacyjną, a rzeczywiste zachowanie zależy od menedżera transakcji i bazy danych.
timeout
Parametr timeout określa maksymalny czas trwania transakcji w sekundach.
@Transactional(timeout = 10)
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
}W powyższym przykładzie ustawiono limit 10 sekund. Jeśli transakcja przekroczy ten czas, menedżer transakcji może zgłosić błąd i doprowadzić do jej wycofania.
Warto jednak zaznaczyć, że timeout nie działa jak uniwersalny mechanizm przerywania kodu Java po określonym czasie. Jego egzekwowanie zależy od używanej technologii, np. Hibernate i sterownika JDBC.
Parametr ten ma zastosowanie podczas rozpoczynania nowej transakcji. Jeśli metoda dołączy do już istniejącej transakcji, nie zmieni jej limitu czasowego.
transactionManager
Parametr transactionManager umożliwia wskazanie konkretnego menedżera transakcji, który powinien obsługiwać daną metodę.
@Transactional(transactionManager = "hibernateTransactionManager")
public void createUser(User user) {
sessionFactory.getCurrentSession().persist(user);
}Jest to przydatne w aplikacjach, które wykorzystują więcej niż jeden menedżer transakcji, np. podczas komunikacji z kilkoma bazami danych.
W typowej aplikacji, w której skonfigurowano tylko jeden menedżer transakcji, nie ma potrzeby jawnego wskazywania tego parametru.
Najczęstsze problemy i dobre praktyki
Chociaż Spring Framework znacząco upraszcza zarządzanie transakcjami, niewłaściwe wykorzystanie adnotacji @Transactional może prowadzić do problemów z wydajnością, nieoczekiwanych rollbacków, a nawet niespójności danych. Poniżej przedstawiam kilka najważniejszych dobrych praktyk.
1. Stosuj @Transactional w warstwie serwisowej
Dobrą praktyką jest umieszczanie adnotacji @Transactional w warstwie serwisowej, ponieważ to właśnie ona odpowiada za realizację logiki biznesowej.
Pozwala to objąć jedną transakcją kilka operacji na bazie danych, nawet jeśli są wykonywane przez różne komponenty DAO.
Nie oznacza to jednak, że każda metoda serwisowa musi być oznaczona @Transactional. Transakcje powinny być stosowane tam, gdzie rzeczywiście są potrzebne.
2. Uważaj na wywoływanie metod wewnątrz tej samej klasy
Jedną z najczęstszych pułapek jest tzw. self-invocation, czyli wywoływanie metody oznaczonej @Transactional z innej metody tej samej klasy.
@Service
public class UserService {
public void createUser(User user) {
saveUser(user);
}
@Transactional
public void saveUser(User user) {
// Zapis użytkownika
}
}W powyższym przykładzie wywołanie saveUser() nie spowoduje rozpoczęcia transakcji, ponieważ odbywa się bezpośrednio wewnątrz klasy, z pominięciem proxy Springa.
Problem można rozwiązać, przenosząc metodę transakcyjną do osobnego komponentu lub umieszczając @Transactional bezpośrednio na metodzie createUser().
Z tego samego powodu należy unikać umieszczania @Transactional na metodach prywatnych. W domyślnym trybie proxy Spring nie przechwytuje takich metod.
3. Nie przechwytuj bezrefleksyjnie wyjątków
Spring domyślnie wykonuje rollback, jeśli z metody transakcyjnej zostanie zgłoszony RuntimeException lub Error.
Przechwycenie wyjątku wewnątrz metody może jednak sprawić, że Spring nie otrzyma informacji o konieczności wycofania transakcji.
Dlatego jeżeli błąd powinien skutkować rollbackiem, najlepiej pozwolić wyjątkowi zostać przekazanym dalej.
Warto również pamiętać o UnexpectedRollbackException. Może on wystąpić, gdy wewnętrzna metoda korzystająca z REQUIRED oznaczy wspólną transakcję jako wymagającą wycofania, ale zewnętrzna metoda spróbuje ją zatwierdzić.
4. Unikaj zbyt długich transakcji
Transakcje powinny trwać możliwie krótko. Długotrwałe transakcje mogą prowadzić do:
- Dłuższego utrzymywania blokad w bazie danych.
- Zwiększonego wykorzystania połączeń JDBC.
- Pogorszenia wydajności aplikacji.
- Większego ryzyka konfliktów między równoległymi operacjami.
Szczególnie należy unikać wykonywania długotrwałych operacji niezwiązanych z bazą danych wewnątrz transakcji, np. wywoływania zewnętrznych API.
Warto również pamiętać, że transakcja bazodanowa nie obejmuje automatycznie operacji wykonywanych w systemach zewnętrznych.
5. Korzystaj z readOnly dla operacji odczytu
Jeśli metoda służy wyłącznie do pobierania danych, warto rozważyć zastosowanie readOnly = true.
@Transactional(readOnly = true)
public User getUser(Long id) {
return sessionFactory.getCurrentSession()
.find(User.class, id);
}W przypadku Hibernate może to ograniczyć narzut związany z mechanizmem wykrywania zmian w encjach.
Należy jednak pamiętać, że readOnly = true nie stanowi gwarancji zablokowania operacji zapisu.
6. Ostrożnie korzystaj z REQUIRES_NEW
Propagacja REQUIRES_NEW pozwala utworzyć niezależną transakcję, ale wiąże się z dodatkowym wykorzystaniem zasobów.
Jeśli metoda wywoływana jest w ramach istniejącej transakcji, Spring zawiesza transakcję zewnętrzną i rozpoczyna nową. Może to wymagać pobrania kolejnego połączenia JDBC z puli.
Zbyt częste korzystanie z REQUIRES_NEW może więc prowadzić do wyczerpania dostępnych połączeń, szczególnie w aplikacjach obsługujących wiele równoległych żądań.
7. Testuj zachowanie transakcji
Samo sprawdzenie, czy metoda poprawnie zapisuje dane, nie wystarczy. Warto również przygotować testy integracyjne weryfikujące zachowanie aplikacji w przypadku błędów.
Szczególnie istotne jest sprawdzenie, czy:
- Zmiany są wycofywane w przypadku wystąpienia odpowiedniego wyjątku.
- Propagacja
REQUIREDiREQUIRES_NEWdziała zgodnie z oczekiwaniami. - Metody oznaczone
@Transactionalsą rzeczywiście wywoływane przez proxy Springa. - Równoczesne operacje na tych samych danych nie prowadzą do utraty aktualizacji.
Dobrze zaprojektowane testy integracyjne pozwalają wykryć błędy, które mogą być trudne do zauważenia podczas zwykłego testowania logiki biznesowej.
Podsumowanie
Spring Framework pozwala zarządzać transakcjami bez konieczności ręcznego ich rozpoczynania, zatwierdzania i wycofywania. Dzięki adnotacji @Transactional programista może skupić się na logice biznesowej, pozostawiając zarządzanie cyklem życia transakcji frameworkowi.
Szczególnie istotne jest zrozumienie propagacji transakcji, która określa zachowanie metod wywoływanych w ramach istniejącej transakcji. Warto również pamiętać o poziomach izolacji, zasadach rollbacku oraz ograniczeniach wynikających z wykorzystania mechanizmu proxy.
W praktyce najważniejsze jest właściwe wyznaczanie granic transakcji, najlepiej na poziomie warstwy serwisowej, oraz unikanie zbyt długich transakcji. Świadome korzystanie z @Transactional pozwala tworzyć aplikacje, które są bardziej niezawodne, wydajne i łatwiejsze w utrzymaniu.
Źródła:
Spring Framework – Transaction Management
