Почему при повторном вызове транзакционного метода в рамках одного объекта транзакция не создается?
Что проверяют
Знаете ли вы, как устроен @Transactional внутри, и понимаете ли ограничение самовызова.
Как отвечать
Потому что @Transactional работает через прокси. Spring не меняет ваш класс: он оборачивает бин динамическим прокси (JDK или CGLIB), и вся транзакционная логика живёт в обёртке. Транзакция открывается, когда вызов проходит через прокси, то есть приходит снаружи.
При самовызове (this.method()) вызов идёт напрямую по ссылке на сам объект, минуя обёртку. Прокси о нём не знает, перехватчик не срабатывает, аннотация игнорируется молча. Ровно по той же причине не работает самовызов с @Cacheable, @Async и @Retryable.
Что из этого следует практически: @Transactional(propagation = REQUIRES_NEW) на методе, вызванном изнутри того же класса, новую транзакцию не создаст, и код продолжит работать в существующей или вовсе без неё.
Как чинить, по убыванию предпочтительности:
- вынести метод в отдельный бин и внедрить его. Самый чистый вариант, заодно обычно означает, что границы классов были неверны;
TransactionTemplate: явное управление транзакцией в коде, без магии аннотаций;- самовнедрение или
ObjectProviderс получением себя же через контейнер, чтобы вызов пошёл через прокси. Работает, но выглядит как обход; AopContext.currentProxy()приexposeProxy = true. Работает, но привязывает код к инфраструктуре AOP;- AspectJ с плетением на этапе компиляции: перехват идёт не через прокси, и самовызов работает. Дорого в настройке.
Тут же стоит назвать соседние ограничения того же происхождения: аннотация на private и final методе не работает (JDK-прокси видит только интерфейсы, CGLIB не может переопределить финальный метод), и на статическом тоже.
Где спотыкаются
Ищут ошибку в настройках транзакций вместо самовызова.
Ставят @Transactional на приватный метод.
Не знают разницы между REQUIRED и REQUIRES_NEW.
Ловят исключение внутри и удивляются, что откат не произошёл: по умолчанию откат идёт только на непроверяемых исключениях.