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: требует существующей транзакции. Если её нет – выбросит исключение.
  • SUPPORTS: работает в рамках транзакции, если она есть, но может быть вызван и без неё.

Пример использования:


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

2.Isolation (уровень изоляции)

Указывает, как транзакция должна видеть изменения, сделанные другими транзакциями. Это важно для работы в многопользовательских системах.

Вот основные уровни изоляции:

  • READ_UNCOMMITTED: транзакция может видеть неприменённые изменения других транзакций (грязное чтение).
  • READ_COMMITTED: исключает грязное чтение.
  • REPEATABLE_READ: защита от немодифицированных чтений (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 — мощный инструмент, снимающий с нас головную боль управления транзакциями вручную. Используйте её ответственно, оптимизируйте параметры и всегда проверяйте работу через тесты!

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ