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

You are currently viewing Co to jest i jak działa @Transactional w Springu?

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.

Subscribe
Powiadom o
guest
2 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Cyberjanusz

public @interface Rollback {

   boolean value() default true;

}

…dlatego wystarczy pisać @Rollback / nie trzeba pisać @Rollback(true)