Как работает @Transactional и почему вызов метода внутри того же класса (self-invocation) может не открыть транзакцию?
- Почему @Transactional не работает при вызове метода из того же класса?
@Transactional реализован прокси: вызов снаружи идёт через прокси, который открывает транзакцию и вызывает метод. Вызов this.other() внутри класса идёт мимо прокси — аннотация на other не срабатывает.
- То же для @Async, @Cacheable, @Retryable и любых аспектов
- Решения: вынести метод в другой бин; внедрить себя (@Lazy self); TransactionTemplate; AspectJ-weaving
- Аннотация на private-методах не работает — прокси их не перехватывает
А ещё Транзакция внешнего метода, если она есть, всё равно действует на внутренний
Подробно
Как работает. Для бина с @Transactional Spring создаёт прокси (CGLIB или JDK). Внешний вызов orderService.place() приходит в прокси; TransactionInterceptor берёт соединение, начинает транзакцию, вызывает настоящий метод, коммитит или откатывает по исключению.
Self-invocation. Внутри метода this — сам объект, а не прокси. Вызов this.saveAudit() с @Transactional(propagation = REQUIRES_NEW) выполнится в текущей транзакции (или без неё), настройки аннотации проигнорированы, ошибок не будет.
@Service
class OrderService {
public void place(Order o) {
save(o); // мимо прокси — @Transactional на save не работает
}
@Transactional
public void save(Order o) { ... }
}
Что делать:
- вынести метод в отдельный бин (лучший вариант — часто это и отдельная ответственность);
- программно:
TransactionTemplate.execute(...); - внедрить собственный прокси (
@Lazy @Autowired OrderService self) — работает, но запутывает; - AspectJ compile-time / load-time weaving — аспекты вплетаются в байткод.
Ещё ограничения прокси: не перехватываются private и final методы (CGLIB), а у JDK-прокси — методы не из интерфейса.
Что хотят услышать
- Прокси как механизм работы @Transactional.
- Почему внутренний вызов идёт мимо прокси.
- Способы решения.
- Другие ограничения прокси.
Частые ошибки
- Ставить @Transactional на private-метод.
- Ожидать REQUIRES_NEW при вызове из того же класса.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Вызов this.other() тоже идёт через прокси, но транзакция стартует только на public-методе
- ✗@Transactional на private-методе работает так же, как на public
- ✗Self-invocation всегда открывает новую транзакцию с propagation REQUIRES_NEW
- ✓Транзакцию открывает прокси, а this.other() идёт мимо него; выход — другой бин
Что спросят дальше
- Какие бывают propagation?
- Чем отличается JDK-прокси от CGLIB?
Разбор темы
- @Transactional: propagation, isolation, rollback
Декларативные транзакции через прокси, ловушка self-invocation, уровни распространения, правила отката и readOnly/isolation.
Связанные вопросы
- Какие ограничения есть у Spring AOP из-за того, что он основан на прокси? Middle
- Когда Spring создаёт JDK dynamic proxy, а когда CGLIB-прокси? Middle
- Что делает propagation=REQUIRES_NEW и какой подводный камень с пулом соединений он несёт? Senior
- Что происходит при выходе из метода с @Transactional, если исключений не было? Junior
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.