В этой статье я бы хотел рассказать о проблемах, которые часто возникают при использовании транзакций в Spring, а в частности, вопросы связанные с использованием аннотации @Transactional.
Начнем с общих вопросов.
Стоит ли использовать эту аннотацию на интерфейсах или на конкретных классах?
Простой ответ - используйте их на конкретных классах. С точки зрения построения сервисов, управление транзакциями - это детали реализации. В очень редких случаях имеет смысл делать транзакции частью контракта, если есть какие-то явные требования к наследованию транзакционных методов (transaction propagation), обьявленых в интерфейсе.
Кроме того, работоспособность аннотаций на интерфейсе не всегда будет корректной. Если стратегия проксирования классов будет выбрана не верно (например, proxy-target-class='true'), то @Transactional на интерфейсе вообще будут игнорироваться. Детальнее об этом можно прочитать в документации Spring.
Будет ли работать @Transactional на приватных методах класса?
Для начала, следует понять каким образом аннотации на методах порождают транзакции. Spring использует AOP для пре/постобработки аннотированных методов. Таким образом все зависит от имплементации AOP и способа вызова метода.
Если используется конфигурация по умолчанию, то класс будет обернут в прокси, и в этом случае только внешние вызовы методов будут транзакционными. А поскольку вызов приватных методов является внутренним для каждого конкретного бина, то @Transactional на нем будет игнорироваться. Аналогичная ситуация будет происходить даже в том случае если метод публичный - вызов такого метода из этого же класса никогда не начнет транзакцию.
Если же бин использует AspectJ AOP, ситуация чуть лучше. В этом случае класс будет "обработан" байткодом, который обеспечит корректный вызов транзакционных вставок на любых методах, не зависимо от области видимости и места вызова.
Почему использовать @Transactional лучше на сервисных классах?
В первую очередь DAO бины, предоставляют примитивные операции над определенным доменом, тем не мнее являются плохими кандидатами для аннотации @Transactional. Конечно, такие транзакции будут очень короткими, это несомненный плюс. Но при этом теряется возможность объеденять несколько мелких операций в один блок, и в случае если, одна из мелких операций даст сбой (rollback), то остальные операции останутся закоммичеными, так как, проходили в отдельных транзакциях. Именно для этого и нужны сервисные бины - обьеденять логически совместимые операции в монолитные блоки. А для того чтобы явно подчеркнуть, что методы DAO бина должны выполнятся в транзакциях обьявленных во внешних сервисах, можно на DAO бин поставить аннотацию @Transactional(propagation = MANDATORY). Такие вспомогательные аннотации помогут быстрее находить ошибки, связанные с пропущенными аннотациями на сервисном уровне.
Стоит ли делать контроллеры транзакционными?
Если ваше приложение маленькое и сервисный слой вырождается в однострочные перевызовы DAO, то возможно, в этом случае и стоит ставить @Transactional на контроллерах. Ну а вобщем случае и в продолжение предыдущего вопроса хотел бы сказать, что мешать MVC и сервисный слой не очень красиво. Кроме того транзакционные контроллеры потребуют дополнительной конфигурации DispatcherServlet контекста и включения в него менеджера транзакций.
Как @Transactional работает в JUnit тестах?
Если Вы используете интеграционные тесты, то @Transactional аннотация обязательно Вам пригодится.
@Transactional аннотацию можно использовать как на уровне класса, так и на отдельных тест методах. В первом случае для каждого тест метода будет создаваться отдельная транзакция, если необходимо отключить использование транзакций для определенных тест методов, можно их пометить аннотацией @NonTransactional.
По умолчанию любой транзакионный тест всегда откатывается после завершения. Это поведение можно поменять либо с помощью вспомогательной аннотации @Rollback(false) применяемой на тесте, либо с помощью аннотации @TransactionConfiguration(defaultRollback=false), которая применяется на JUnit классе.
Следует отметить, что методы аннотированные JUnit аннотациями @Before/@After будут также запускаться в транзакции. Потому, если есть необходимость сделать какие-то подготовительные работы вне транзакции, их следует вынести в отдельные методы и пометить специальными аннотациями @BeforeTransaction/@AfterTransaction. Правда, не забудьте, что такие методы не будут запускаться вообще для нетранзакционных тест методов.
Почему некоторые обьекты остаются в базе после выполнения @Transactional Unit теста?
Самая частая причина - это использование @Transactional(propagation = Propagation.REQUIRES_NEW) в тестируемых классах. Эта аннотация указывает менеджеру транзакций открыть новую транзакцию при входе в метод, вне зависимости от того, существует уже открытая внешняя транзакция или нет. Поэтому все действия выполненные в таком методе не будут откатаны после завершеня тест метода.
This comment has been removed by the author.
ReplyDeleteЕвгений, а как Вы предлагаете обрабатывать Exceptions уровня транзакции, навроде ConstraintViolationException? Ведь если методы сервиса помечены как @Transactional, то значит коммит будет совершён уже после того, как метод будет отработан. Неужели обрабатывать исключение ConstraintViolationException в контроллере? Как-то и уровень абстракции уже не тот, да и какую путную информацию можно будет извлечь без знания конкретного контекста (если исключение произошло в толстом и нагруженном логикой бизнес-методе сервиса)? Вот пример - методе OrderingService.processOrder даёт такое исключение, которое может означать, что заказ с таким номером уже есть, или что не удаётся списать товар со склада, или что-то ещё. Если такое исключение произойдёт во время вызова операции DAO и будет обработано в сервисе, то скорее всего можно будет определить проблему. Но вне метода сервиса как быть? (Ведь если метод сервиса @Transactional, то исключение произойдёт уже вне его, в аспекте, совершающем коммит) Или есть способы обрабатывать исключения по-другому?
ReplyDeleteСмотря что вы хотите обрабатывать. Если задача показать красивое сообщение, то в контроллере ему самое место. А ещё лучше в @ControllerAdvice. Если нужна бизнес обработка с последующей работой с БД, то я бы @Transactional вообще не использовал
DeleteСмотря что вы хотите обрабатывать. Если задача показать красивое сообщение, то в контроллере ему самое место. А ещё лучше в @ControllerAdvice. Если нужна бизнес обработка с последующей работой с БД, то я бы @Transactional вообще не использовал
DeleteThis comment has been removed by the author.
ReplyDelete