JavaRush /Курси /Модуль 5. Spring /Анотація @Transactional: основні можливості

Анотація @Transactional: основні можливості

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

Уявіть собі ситуацію: потрібно реалізувати механізм переказу грошей між двома банківськими рахунками. Очевидно, що операція складається з двох кроків:

  1. Списання коштів з одного рахунку.
  2. Зарахування коштів на інший рахунок.

Якщо в процесі щось піде не так, наприклад, збій на другому кроці, важливо відкотити всю операцію, щоб уникнути ситуації, коли гроші "зникли". Тут у гру вступає анотація @Transactional, дозволяючи вам зосередитися на бізнес-логіці, а сам Spring бере на себе управління транзакцією, роблячи код чистішим і простішим.


Як працює @Transactional?

@Transactional — це анотація, яка задає поведінку транзакції для методу або класу. Spring використовує аспектно-орієнтоване програмування (AOP), щоб обгорнути ваш код у "транзакційні проксі". Це означає, що коли метод запускається, Spring автоматично починає транзакцію, а по завершенні методу — або фіксує зміни (commit), або відкотить їх (rollback), якщо сталося виключення.

Ось приклад використання @Transactional для операції переказу грошей між рахунками:


@Service
public class BankService {

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // Крок 1: Списання коштів з одного рахунку
        accountRepository.debit(fromAccountId, amount);

        // Крок 2: Зарахування коштів на інший рахунок
        accountRepository.credit(toAccountId, amount);
    }
}

Просто? Але це лише верхівка айсберга.


Опції анотації @Transactional

Анотація @Transactional надає багато опцій для керування поведінкою транзакцій. Давайте розберемо найважливіші.

1. Propagation (поширення)

Це вказує, як має виконуватися транзакція, якщо метод викликається всередині іншої транзакції. Наприклад:

  • REQUIRED (за замовчуванням): Використовує існуючу транзакцію, якщо вона є. Якщо ні, створює нову.
  • REQUIRES_NEW: завжди створює нову транзакцію, призупиняючи поточну.
  • MANDATORY: вимагає існуючої транзакції. Якщо її немає – кинe виключення.
  • SUPPORTS: працює в рамках транзакції, якщо вона є, але може бути викликаний і без неї.

Приклад використання:


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
    auditLogRepository.save(log);
}

2. Isolation (рівень ізоляції)

Вказує, як транзакція повинна бачити зміни, зроблені іншими транзакціями. Це важливо для роботи в мультикористувацьких системах.

Ось основні рівні ізоляції:

  • READ_UNCOMMITTED: транзакція може бачити непідтверджені зміни інших транзакцій (dirty read).
  • READ_COMMITTED: виключає dirty read.
  • REPEATABLE_READ: захист від non-repeatable reads (consistent reads).
  • SERIALIZABLE: повна ізоляція, максимальний захист від конфліктів, але зі зниженням продуктивності.

Приклад:


@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
    // Критично важлива операція
}

3. Timeout (час виконання)

Іноді транзакції можуть блокуватися або працювати надто довго. Ви можете задати ліміт часу (у секундах), після якого транзакція буде перервана.

Приклад:


@Transactional(timeout = 5) // Максимум 5 секунд
public void executeLongQuery() {
    // Довга операція
}

4. ReadOnly (тільки для читання)

Якщо транзакція тільки читає дані (без змін), ви можете вказати readOnly=true. Це може оптимізувати продуктивність.

Приклад:


@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
    return accountRepository.findAll();
}
УВАГА:
Хоча цей варіант вказує, що метод не повинен змінювати дані, на рівні бази даних це не завжди означає, що запити стануть швидшими.

5. RollbackFor (відкат транзакції при помилці)

За замовчуванням Spring відкачує транзакцію лише у випадку RuntimeException або Error. Якщо ви хочете відкотити транзакцію при інших виключеннях (наприклад, Exception), потрібно вказати це явно:


@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
    // Код, який може кинути виключення
}

Реальні приклади використання

Приклад 1: Обробка замовлень в інтернет-магазині

Уявіть обробку замовлення, яка включає:

  • Списання товару зі складу
  • Зняття коштів з картки клієнта
  • Запис даних про замовлення

@Service
public class OrderService {

    @Transactional
    public void processOrder(Order order) {
        // Перевірка наявності товару
        stockService.decreaseStock(order.getProductId(), order.getQuantity());

        // Списання коштів
        paymentService.charge(order.getPaymentInfo());

        // Збереження замовлення
        orderRepository.save(order);
    }
}

Якщо на будь-якому етапі станеться помилка, всі зміни будуть відкоті.

Приклад 2: Логування помилок

Іноді потрібно зберігати журнал подій поза основною транзакцією. Використаємо REQUIRES_NEW:


@Service
public class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logError(String message) {
        errorLogRepository.save(new ErrorLog(message));
    }
}

Типові помилки при роботі з @Transactional

  1. Анотація не спрацювала. Це може статися, якщо метод викликано з того ж класу (через this). Пам'ятайте, що Spring застосовує проксі, і виклик зсередини одного класу оминає механізм AOP. Рішення? Виносьте метод в інший @Service.
  2. Поєднання зовнішніх і внутрішніх транзакцій. Наприклад, використання @Transactional(propagation = Propagation.REQUIRES_NEW) всередині методу може створювати накладні витрати. Використовуйте тільки за потреби.
  3. Неправильна поведінка rollback. Якщо ви забули вказати rollbackFor, Spring може не відкотити транзакцію. Завжди явно вказуйте потрібну поведінку.

Практика: Впровадження @Transactional в додаток

Створимо метод для обробки замовлення з автоматичним відкатом транзакції у випадку помилки:


@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(Long productId, int quantity) {
        // Зменшуємо кількість товару
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Продукт не знайдено!"));

        if (product.getStock() < quantity) {
            throw new RuntimeException("Не вистачає товару на складі!");
        }

        product.setStock(product.getStock() - quantity);
        productRepository.save(product);

        // Зберігаємо замовлення
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        // Штучний збій для перевірки rollback
        if (quantity > 10) {
            throw new RuntimeException("Збій при обробці замовлення!");
        }
    }
}

Перевірка:

  1. Виклик методу placeOrder з коректними даними — створить замовлення і зменшить кількість товару.
  2. Запуск з великою кількістю (quantity > 10) — викличе збій. Spring відкотить зміни, залишивши дані консистентними.

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

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