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() не создаст границу при таком внутреннем вызове. Надёжные варианты:
- вынести
saveBatchв отдельный Spring-бин; - сделать внешнюю операцию
runтранзакционной, если она должна быть атомарной; - использовать
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.