Transactional

@Transactional задаёт границу транзакции Spring. Обычно инфраструктура создаёт proxy: до вызова метода он получает соединение и начинает или присоединяет транзакцию, после вызова фиксирует изменения либо выполняет rollback.

@Service
class TransferService {
    private final AccountRepository accounts;
 
    TransferService(AccountRepository accounts) {
        this.accounts = accounts;
    }
 
    @Transactional
    public void transfer(long from, long to, BigDecimal amount) {
        Account source = accounts.getRequired(from);
        Account target = accounts.getRequired(to);
        source.withdraw(amount);
        target.deposit(amount);
    }
}

Обе записи должны завершиться как одна операция. Свойства самой БД описаны в Транзакции принцип ACID; Spring управляет границей и выбирает PlatformTransactionManager.

Основные параметры

  • propagation — как метод взаимодействует с уже открытой транзакцией;
  • isolation — какие конкурентные изменения видит транзакция;
  • timeout — предел времени;
  • readOnly — подсказка инфраструктуре и ORM, а не универсальный запрет записи;
  • rollbackFor и noRollbackFor — правила отката.

Обычно используется REQUIRED: присоединиться к существующей транзакции или начать новую. REQUIRES_NEW приостанавливает внешнюю и создаёт независимую; это меняет атомарность и требует ещё одного соединения. Подробнее: Propagation и Изоляция транзакций.

Правила rollback

По умолчанию Spring откатывает транзакцию при RuntimeException и Error, но не при checked exception.

@Transactional(rollbackFor = IOException.class)
public void importFile(Path file) throws IOException {
    // ...
}

Если исключение поймано и не вышло за proxy-границу, interceptor считает вызов успешным. Тогда нужно либо пробросить ошибку, либо явно определить результат транзакции. Не следует использовать исключения как обычное ветвление.

Proxy и self-invocation

@Service
class ImportService {
    public void run() {
        saveBatch(); // this.saveBatch(): обход proxy
    }
 
    @Transactional
    public void saveBatch() {
        // ...
    }
}

В proxy-режиме аннотация на saveBatch() не создаст границу при таком внутреннем вызове. Надёжные варианты:

  1. вынести saveBatch в отдельный Spring-бин;
  2. сделать внешнюю операцию run транзакционной, если она должна быть атомарной;
  3. использовать TransactionTemplate для программной границы.

Получать самого себя из ApplicationContext или внедрять self-reference обычно не стоит: зависимость и порядок вызова становятся скрытыми.

Транзакция и жизненный цикл JPA

Внутри транзакции сущности находятся в persistence context. Hibernate отслеживает изменения и выполняет flush перед commit либо раньше, если это нужно запросу. Ошибка ограничения может возникнуть не на save, а на flush/commit.

save() репозитория не означает немедленный SQL. И наоборот, чтение ленивой связи после закрытия persistence context приводит к LazyInitializationException.

Границы транзакции

Хорошая граница обычно совпадает с одним прикладным use case. Не следует:

  • держать транзакцию во время долгого HTTP-вызова;
  • включать пользовательское ожидание или тяжёлый расчёт;
  • считать запись в БД и публикацию в Kafka одной атомарной операцией без специального паттерна;
  • открывать транзакцию в controller только ради lazy-связей.

Для согласования БД и брокера применяют, например, transactional outbox, а не надежду на две независимые фиксации.

На собеседовании

Почему @Transactional может не сработать? Объект создан не Spring, вызов обошёл proxy, метод не подходит для proxy-перехвата, выбран не тот transaction manager или исключение было поймано.

Когда фактически выполняется commit? После успешного возврата из метода через interceptor; SQL может быть отправлен раньше при flush.

Гарантирует ли readOnly = true отсутствие UPDATE? Нет. Поведение зависит от менеджера транзакций, драйвера и БД; это прежде всего декларация намерения и возможная оптимизация.

Связанные заметки: Как Spring работает с транзакциями, Spring AOP, Spring Proxy, Spring Data JPA.