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

Ответ

Аннотация @Transactional в Spring декларативно управляет границами транзакций для методов или классов. При вызове аннотированного метода Spring создает или присоединяется к существующей транзакции, а по завершении — фиксирует (commit) или откатывает (rollback) изменения.

Основной принцип:

  1. Перед методом: Открывается транзакция (или используется существующая).
  2. Во время метода: Все операции с БД выполняются в этой транзакции.
  3. После метода: Если метод завершился успешно (без исключения) — транзакция фиксируется. Если было выброшено исключение — транзакция откатывается.

Ключевые параметры аннотации:

  • propagation — определяет поведение при вложенных вызовах. Например:
    • REQUIRED (по умолчанию): использует текущую транзакцию или создает новую.
    • REQUIRES_NEW: всегда создает новую транзакцию, приостанавливая текущую.
  • isolation — уровень изоляции транзакции (например, READ_COMMITTED).
  • rollbackFor / noRollbackFor — указывает, для каких исключений делать или не делать откат.
  • readOnlytrue для оптимизации операций только на чтение.
  • timeout — таймаут транзакции в секундах.

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

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

    @Transactional(rollbackFor = {InsufficientFundsException.class})
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Недостаточно средств");
        }

        from.debit(amount); // Списываем
        to.credit(amount);  // Зачисляем
        // Оба вызова save() выполняются в одной транзакции.
        // При исключении балансы не изменятся.
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

Важные ограничения:

  • Работает только на public-методах из-за механизма проксирования AOP.
  • По умолчанию откатывает транзакцию только для unchecked (RuntimeException) и Error исключений. Для checked-исключений отката не будет.
  • Самовызов метода внутри одного класса (без прокси) не запустит транзакционное поведение.

Ответ 18+ 🔞

Давай разберём эту вашу транзакционную магию, а то народ путается, как хуй в проруби.

Вот смотри, есть у тебя метод, который должен делать несколько операций с базой. Например, перевести бабки с одного счёта на другой. Если первую операцию выполнил, а на второй — пиздец, исключение вылетело, то получается хуйня: деньги уже списали, а на другой счёт не зачислили. Бабки просто испарились, как твоя зарплата в пятницу вечером.

Чтобы такого не было, умные дядьки придумали транзакции. А в Spring за её управление отвечает аннотация @Transactional. Она, блядь, как надёжный друг: либо всё делает как надо, либо, если что-то пошло не так, откатывает всё к хуям, будто ничего и не было.

Как это, сука, работает?

  1. Заходишь в метод — Spring открывает транзакцию (или цепляется к уже открытой, если она есть).
  2. Выполняешь свои штуки — все запросы к базе внутри этой транзакции.
  3. Выходишь из метода — если вышел красиво, без скандала, Spring говорит базе: «Всё, чувак, фиксируй (commit), я доволен». Если же вылетело исключение — Spring орёт: «Откатывай всё нахуй (rollback), тут пиздец случился!».

На что можно смотреть в этой аннотации?

  • propagation — решает, что делать, когда транзакция встречает другую транзакцию. Как в подъезде два мужика. По умолчанию стоит REQUIRED: «Брат, если ты уже в процессе, давай вместе, а если нет — я начну». А есть REQUIRES_NEW — это такой похуист: «Отъебись, я свою заведу, а твою на паузу поставлю».
  • isolation — уровень изоляции. Чтоб другие транзакции не мешали, как назойливые соседи. READ_COMMITTED — стандартный, чтобы не видеть не зафиксированный другими говнокод.
  • rollbackFor / noRollbackFor — тут указываешь, от каких исключений откатываться, а от каких — нет. По умолчанию откатывается от RuntimeException и Error. А если твой метод кидает какое-нибудь своё SuperImportantCheckedException, и откатываться от него не надо — указываешь в noRollbackFor.
  • readOnly — ставишь true, если метод только читает. Это как табличку «руками не трогать» для базы, может немного ускорить.
  • timeout — время в секундах, сколько транзакция может мучать базу. Если не успела — ей кирдык.

Вот живой пример, чтобы не быть пиздаболом:

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

    @Transactional(rollbackFor = {InsufficientFundsException.class})
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Недостаточно средств");
        }

        from.debit(amount); // Списываем
        to.credit(amount);  // Зачисляем
        // Оба вызова save() в одной транзакции.
        // Кинет исключение — ни хуя не спишется.
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

А теперь, блядь, главные подводные ебланы:

  1. Работает только на public-методах. Потому что Spring накидывает свою магию (прокси) поверх класса, а приватные методы ему не видны. Вызовишь изнутри класса — транзакции не будет, будет просто вызов метода. Пиздец, да?
  2. По умолчанию откатывает только на unchecked исключениях (RuntimeException) и Error. Если твой метод объявляет в сигнатуре throws SQLException (checked), и оно вылетит — транзакция закоммитится, а деньги твои сгорят. Поэтому для таких случаев явно указывай rollbackFor = SQLException.class.
  3. Самовызов внутри класса не работает. Если метод a() вызывает метод b() с @Transactional внутри одного и того же бина — транзакция для b() не откроется. Потому что Spring-прокси тут не при чём, это прямой вызов. Решение — выноси b() в отдельный бин или используй AopContext.currentProxy(), но это уже, прости господи, извращение.

Короче, инструмент мощный, но если не понимать, как он устроен внутри, можно наломать таких дров, что потом пол-проекта переписывать. Держи ухо востро.