Как работает аннотация @Transactional в Spring Framework?

Ответ

Аннотация @Transactional в Spring предоставляет декларативное управление транзакциями на основе AOP (Aspect-Oriented Programming). Она избавляет разработчика от написания шаблонного кода для открытия, коммита и отката транзакций.

Ключевые атрибуты и их поведение:

  • propagation (распространение): Определяет, как должна вести себя транзакция при наличии уже существующей.
    • REQUIRED (по умолчанию): Использует текущую транзакцию или создаёт новую.
    • REQUIRES_NEW: Всегда создаёт новую транзакцию, приостанавливая текущую.
    • NESTED: Создаёт вложенную транзакцию внутри текущей.
  • isolation (изоляция): Определяет уровень изоляции транзакции (например, READ_COMMITTED).
  • readOnly: Указывает, что транзакция только для чтения (может оптимизировать работу).
  • timeout: Время (в секундах), после которого транзакция будет откатана.
  • rollbackFor / noRollbackFor: Указывает, для каких исключений выполнять или не выполнять откат.

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

@Service
public class TransferService {
    @Autowired
    private AccountRepository repository;

    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.READ_COMMITTED,
                   rollbackFor = {InsufficientFundsException.class, DataAccessException.class})
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = repository.findById(fromId).orElseThrow();
        Account to = repository.findById(toId).orElseThrow();

        from.debit(amount); // Списание
        to.credit(amount);  // Зачисление

        repository.save(from);
        repository.save(to);
        // При любом unchecked исключении или указанном в rollbackFor произойдёт автоматический rollback.
    }
}

Важные нюансы:

  • Проксирование: @Transactional работает через динамические прокси (или AspectJ), поэтому вызов метода @Transactional изнутри того же класса (например, через this.internalMethod()) не будет перехвачен прокси, и транзакция не откроется. Нужно использовать инъекцию самого бина или @Transactional на вызываемом публичном методе.
  • Тестирование: В интеграционных тестах можно использовать @Transactional для автоматического отката изменений после каждого теста.
  • Менеджеры транзакций: Для работы требуется правильно настроенный PlatformTransactionManager (например, DataSourceTransactionManager для JDBC).

Ответ 18+ 🔞

Так, слушай, смотри, я тут про эту вашу @Transactional в Spring расскажу, чтоб ты понял, насколько это офигенная штука, если не накосячить.

Представь, что каждый раз, когда ты пишешь в базу, тебе надо вручную открывать транзакцию, потом коммитить, а если что-то пошло не так — откатывать. Это же пиздец какой-то, да? Ну вот, чтобы не писать эту хуйню каждый раз, умные дядьки придумали эту аннотацию. Она, как волшебный плащ-невидимка, оборачивает твой метод и делает всю грязную работу сама. Магия, блядь, на основе AOP.

Смотри, какие у неё есть кнопки-переключатели (атрибуты):

  • propagation (распространение): Это про то, что делать, если транзакция уже есть.

    • REQUIRED (стоит по умолчанию): Если транзакция уже идёт — юзаем её. Нету — создаём новую. Самый частый сценарий, в рот меня чих-пых.
    • REQUIRES_NEW: А вот это уже поинтереснее. Она говорит: «Похуй на текущую транзакцию, я создам свою, новую, а старую на паузу поставлю». Полезно, когда тебе надо что-то записать в лог, даже если основная операция откатится.
    • NESTED: Создаёт вложенную транзакцию. Если в ней пиздец — откатится только она, а основная может жить дальше. Красота.
  • isolation (изоляция): Ну, это классика. READ_COMMITTED, REPEATABLE_READ и прочая хуйня. Чтоб фантомные чтения и прочие глюки не вылезали.

  • readOnly: Ставишь true и намекаешь базе, что ты только читать будешь. Она иногда может оптимизироваться, но это не точно.

  • timeout: Через сколько секунд твою долгую транзакцию пристрелят и откатят. Чтоб не висела, сука, вечность.

  • rollbackFor / noRollbackFor: А вот это важно! Говоришь, за какие исключения откатываться (InsufficientFundsException.class), а за какие — нет. По умолчанию откат только на unchecked исключениях (RuntimeException и его дети). А SQLException — это checked, так что если не указал — транзакция закоммитится, а потом будет пиздец. Запомни это, ебта!

Вот смотри, как это выглядит в коде:

@Service
public class TransferService {
    @Autowired
    private AccountRepository repository;

    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.READ_COMMITTED,
                   rollbackFor = {InsufficientFundsException.class, DataAccessException.class})
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = repository.findById(fromId).orElseThrow();
        Account to = repository.findById(toId).orElseThrow();

        from.debit(amount); // Списание
        to.credit(amount);  // Зачисление

        repository.save(from);
        repository.save(to);
        // Если тут вылетит InsufficientFundsException или что-то из DataAccessException — всё откатится автоматом. Магия!
    }
}

А теперь, блядь, главный подводный камень, про который все обоссываются:

Всё это работает через прокси. Это значит, что когда ты вызываешь метод transferMoney из другого бина — всё ок. Но если ты вызовешь его изнутри того же самого класса, например, через this.transferMoney(...) или из другого не-проксированного метода — то прокси-обёртка не сработает, и транзакция НЕ ОТКРОЕТСЯ. Вообще. Ни хуя.

Представь: ты внутри своего сервиса вызываешь приватный метод, а он без транзакции. Пиздец и разочарование. Решение? Или выносить в отдельный бин, или ставить @Transactional на публичный метод, который ты вызываешь.

И да, чтоб эта красота работала, нужен правильно настроенный менеджер транзакций (PlatformTransactionManager), типа DataSourceTransactionManager. Без него — просто аннотация на бумажке.

Вот так вот, коротко и с матом. Главное — не наебаться с вызовами внутри класса и не забыть про rollbackFor для checked исключений.