JavaRush /Курси /Модуль 5. Spring /Декларативне управління транзакціями через анотації

Декларативне управління транзакціями через анотації

Модуль 5. Spring
Рівень 6 , Лекція 3
Відкрита

Уяви, що доводилося б вручну відкривати й закривати транзакції щоразу, щойно в коді щось йде не так. Це приблизно як вручну сортувати величезні масиви замість того, щоб користуватися вбудованими методами Java. Можна, але навіщо морочитися?

Декларативне управління транзакціями дозволяє нам зовсім відмовитись від явного коду управління транзакціями (напр., beginTransaction і commit) і зосередитись на логіці самої аплікації. Spring бере на себе всю рутину — створення, управління і відкат транзакцій.

Переваги декларативного підходу

  1. Підвищена читабельність коду: код аплікації стає чистішим, бо не потрібно явно прописувати логіку управління транзакціями.
  2. Менше багів: ризик забути закрити транзакцію мінімальний, адже цим керує Spring.
  3. Більше часу на важливе: замість боротьби з транзакціями ти можеш зосередитись на бізнес-логіці (або на піці — вирішувати тобі).

Декларативне управління транзакціями в Spring

Тепер подивимось, як Spring забезпечує декларативне управління транзакціями через анотації. За кулісами працює AOP (аспектно-орієнтоване програмування). Spring створює проксі-об'єкти твоїх компонентів і додає логіку управління транзакціями в точки входу методів. Цей підхід називається "декларативним", бо ми декларуємо параметри транзакції, а Spring робить усе інше.

Як працює транзакція через @Transactional?

  • Коли метод, позначений анотацією @Transactional, викликається, створюється проксі (замісник), який керує транзакцією.
  • Проксі перехоплює виклик методу, відкриває транзакцію і гарантує, що у випадку виключення транзакція буде відкотена.
  • Якщо метод успішно завершується, транзакція фіксується (committed).

Приблизно так виглядає під капотом взаємодія:


Метод викликається -> Проксі перевіряє @Transactional -> Відкривається транзакція ->
-> Виконується метод -> Успіх? commit() : rollback()

Налаштування декларативного управління

1. Підключення керування транзакціями

Щоб увімкнути транзакції в проєкті потрібно зробити всього кілька кроків:

  1. Додай залежність на spring-tx у свій pom.xml або build.gradle (якщо ти використовуєш Spring Boot, вона вже там):
    
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
    </dependency>
    
  2. Увімкни підтримку анотації @Transactional в аплікації. Для цього додай у конфігураційний клас анотацію @EnableTransactionManagement:
    
    @Configuration
    @EnableTransactionManagement
    public class AppConfig {
        // Тут можна налаштувати bean-и твоєї аплікації
    }
    

2. Використання @Transactional в методах

Після налаштування можна помічати методи анотацією @Transactional і керувати там транзакціями.

Ось приклад:


@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // Крок 1: Збереження замовлення
        orderRepository.save(order);

        // Крок 2: Оновлення балансу користувача
        userAccountService.decreaseBalance(order.getUserId(), order.getTotalPrice());

        // Навмисно створимо помилку (наприклад, ділення на нуль)
        int error = 1 / 0;

        // Якби не @Transactional, зміни могли б зберегтися в базі,
        // але транзакція автоматично відкотиться через виключення.
    }
}

Тут ми навмисно ламаємо код, ділячи на нуль. Але завдяки анотації @Transactional все відкотиться! Користувач не втратить гроші, а база залишиться в цілості.


3. Гнучкість налаштувань @Transactional

Параметри анотації: @Transactional дозволяє вказати багато параметрів:

  • propagation: як транзакція має поводитися в контексті "взаємних викликів методів".
  • isolation: рівень ізоляції транзакції (наприклад, READ_COMMITTED, SERIALIZABLE).
  • rollbackFor: виключення, при яких має виконуватися відкат.
  • timeout: таймаут транзакції.
  • readOnly: якщо транзакція тільки читає дані, ставимо true.

Приклад налаштування параметрів:


@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.SERIALIZABLE,
    timeout = 5,
    rollbackFor = {SQLException.class, CustomException.class},
    readOnly = false
)
public void processOrder(Order order) {
    // Твій код
}

Приклад з propagation

Spring підтримує кілька типів поведінки транзакцій. Наприклад:

  • REQUIRED: використати існуючу транзакцію або створити нову.
  • REQUIRES_NEW: завжди створювати нову транзакцію.
  • NESTED: вкладені транзакції.

Приклад різної поведінки:


@Service
public class PaymentService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void processPayment() {
        // Код для обробки платежу
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPayment() {
        // Код для логування транзакції
    }
}

Як Spring реалізує декларативне управління? (Під капотом)

Spring використовує AOP і створює проксі-об'єкт. Це означає, що коли ти викликаєш метод з анотацією @Transactional, реальний виклик проходить через проксі, який додає управління транзакцією. Наприклад, метод загортається в щось на кшталт:


transactionManager.begin();
try {
    method(); // Виклик реального методу
    transactionManager.commit();
} catch (Exception e) {
    transactionManager.rollback();
}

Ця магія дозволяє нам уникнути ручного керування транзакціями.


Практичний приклад: управління транзакціями в e-commerce аплікації

Давай створимо простий e-commerce сценарій, де користувач оформляє замовлення. Ми будемо використовувати @Transactional для управління всіма операціями: збереження замовлення, оновлення балансу користувача.


@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private UserAccountService userAccountService;

    @Transactional
    public void placeOrder(Order order) {
        // Зберігаємо замовлення
        orderRepository.save(order);

        // Оновлюємо баланс користувача
        userAccountService.decreaseBalance(order.getUserId(), order.getTotalPrice());

        // Навмисне створення помилки
        if (order.getTotalPrice() > 10000) {
            throw new RuntimeException("Бюджет перевищено!");
        }
    }
}

При перевищенні бюджету транзакція відкотиться. Ми залишаємося з узгодженими даними і задоволеним користувачем.

Перевіряємо відкат вручну

Можеш написати тест, щоб перевірити, що дані відкотяться при виникненні виключення:


@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    public void testTransactionRollback() {
        Order order = new Order(/* дані замовлення */);

        try {
            orderService.placeOrder(order);
        } catch (RuntimeException e) {
            // Ловимо RuntimeException
        }

        // Переконуємося, що замовлення НЕ збережено в базі
        assertEquals(0, orderRepository.count());
    }
}

Типові помилки і підводні камені

  1. Не працює @Transactional?
    • Перевір, чи ввімкнув ти @EnableTransactionManagement. Без цього анотація працювати не буде.
    • Переконайся, що метод з @Transactional викликається через Spring Bean, інакше проксі не буде використовуватись.
  2. Не відкотиться транзакція?
    • Spring за замовчуванням відкатує лише RuntimeException. Якщо хочеш обробляти інші виключення, використовуй параметр rollbackFor.
  3. Вкладені транзакції можуть заплутати:
    • Пам'ятай, що propagation = REQUIRES_NEW створює нову транзакцію, тоді як REQUIRED продовжує поточну.

На цьому етапі ти готовий керувати транзакціями за допомогою анотації @Transactional. Хай твої дані завжди будуть узгоджені, а системи — відмовостійкими!

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ