Как использовать аннотацию @Transactional в Spring?

Ответ

Аннотация @Transactional декларативно управляет границами транзакций в Spring. Она применяется на уровне метода или класса.

Базовый пример:

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

    @Transactional // Весь метод выполняется в одной транзакции
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(fromId).orElseThrow();
        Account toAccount = accountRepository.findById(toId).orElseThrow();

        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(); // RuntimeException вызовет rollback
        }

        fromAccount.debit(amount);
        toAccount.credit(amount);
        // Сохранение не требуется явно, если используется контекст persistence с flush-mode AUTO (по умолчанию в @Transactional)
    }
}

Ключевые атрибуты для настройки:

  • propagation: Определяет поведение транзакции относительно существующего контекста (например, REQUIRED, REQUIRES_NEW).
  • isolation: Уровень изоляции (например, READ_COMMITTED, SERIALIZABLE).
  • rollbackFor / noRollbackFor: Какие исключения вызывают или не вызывают откат.
  • readOnly: Флаг только для чтения (оптимизация для true).
  • timeout: Таймаут транзакции в секундах.

Важно: @Transactional работает через AOP-прокси, поэтому вызовы методов внутри одного класса не будут перехвачены прокси. По умолчанию откат происходит только при непроверяемых исключениях (RuntimeException и его наследниках).

Ответ 18+ 🔞

Слушай, а вот эта ваша аннотация @Transactional — это ж просто магия какая-то, ей-богу. Накропал её над методом, и всё — Spring сам, как нянька, оборачивает твой код в транзакцию. Не нужно руками открывать, коммитить, откатывать — красота, пиздец.

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

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

    @Transactional // Вот этот волшебный пиздёж. Весь метод — одна транзакция.
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(fromId).orElseThrow();
        Account toAccount = accountRepository.findById(toId).orElseThrow();

        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(); // Кинул RuntimeException — и всё, блядь, откат!
        }

        fromAccount.debit(amount);
        toAccount.credit(amount);
        // Сохранять вручную не надо, если всё настроено. Живёшь как в сказке.
    }
}

Но это, конечно, база. А если копнуть, там же овердохуища настроек, на любой вкус. Главные ручки, за которые можно крутить:

  • propagation: Это про то, как твоя транзакция будет вести себя, если её уже кто-то начал. Например, REQUIRED — «ой, есть транзакция? Отлично, буду в ней работать». А REQUIRES_NEW — это «пошёл нахуй, я свою заведу, отдельную». Очень важная штука, если не хочешь неожиданных сюрпризов.
  • isolation: Уровень изоляции, ну ты понял. READ_COMMITTED обычно хватает, но если у тебя там какие-то гонки ебаные, можно и SERIALIZABLE вкрутить — будет медленнее, но надёжнее, как швейцарские часы, блядь.
  • rollbackFor / noRollbackFor: А вот это ключевое! По умолчанию откат — только на непроверяемых исключениях (RuntimeException и дети). А если твой метод кидает какое-нибудь своё CheckedException, и ты хочешь откат — указывай его в rollbackFor. И наоборот, если какой-нибудь NullPointerException по твоей бизнес-логике не должен всё ломать — пиши в noRollbackFor. Иначе — пиздец и неразбериха.
  • readOnly: Выставляешь в true, если метод только читает. Это как флажок для базы и фреймворка — они могут немного схитрить и оптимизироваться.
  • timeout: Сколько секунд транзакция может болтаться. Если дольше — кидается исключение и откат. Чтобы какой-нибудь долбоёбский запрос не висел до скончания времён.

И главный подводный камень, ёпта: Вся эта магия работает через AOP-прокси. Это значит, что если ты внутри одного класса вызовешь один @Transactional-метод из другого — то второй вызов НЕ БУДЕТ обёрнут в транзакцию! Прокси-то смотрит только на вызовы извне. Так что не выёбывайся, организуй код правильно.

Короче, инструмент мощный, но с мозгом использовать надо. А то накропаешь везде @Transactional и будешь потом гадать, почему данные не сохраняются или, наоборот, откатывается то, что не должно.