2012-08-07

FAQ: Использование @Transactional аннотации

В этой статье я бы хотел рассказать о проблемах, которые часто возникают при использовании транзакций в 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) в тестируемых классах. Эта аннотация указывает менеджеру транзакций открыть новую транзакцию при входе в метод, вне зависимости от того, существует уже открытая внешняя транзакция или нет. Поэтому все действия выполненные в таком методе не будут откатаны после завершеня тест метода.

5 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Евгений, а как Вы предлагаете обрабатывать Exceptions уровня транзакции, навроде ConstraintViolationException? Ведь если методы сервиса помечены как @Transactional, то значит коммит будет совершён уже после того, как метод будет отработан. Неужели обрабатывать исключение ConstraintViolationException в контроллере? Как-то и уровень абстракции уже не тот, да и какую путную информацию можно будет извлечь без знания конкретного контекста (если исключение произошло в толстом и нагруженном логикой бизнес-методе сервиса)? Вот пример - методе OrderingService.processOrder даёт такое исключение, которое может означать, что заказ с таким номером уже есть, или что не удаётся списать товар со склада, или что-то ещё. Если такое исключение произойдёт во время вызова операции DAO и будет обработано в сервисе, то скорее всего можно будет определить проблему. Но вне метода сервиса как быть? (Ведь если метод сервиса @Transactional, то исключение произойдёт уже вне его, в аспекте, совершающем коммит) Или есть способы обрабатывать исключения по-другому?

    ReplyDelete
    Replies
    1. Смотря что вы хотите обрабатывать. Если задача показать красивое сообщение, то в контроллере ему самое место. А ещё лучше в @ControllerAdvice. Если нужна бизнес обработка с последующей работой с БД, то я бы @Transactional вообще не использовал

      Delete
    2. Смотря что вы хотите обрабатывать. Если задача показать красивое сообщение, то в контроллере ему самое место. А ещё лучше в @ControllerAdvice. Если нужна бизнес обработка с последующей работой с БД, то я бы @Transactional вообще не использовал

      Delete
  3. This comment has been removed by the author.

    ReplyDelete