Почему мы не должны создавать Spring контроллер MVC @Transactional?
Уже есть несколько вопросов по теме, но никакой ответ вообще не дает аргументов, чтобы объяснить, почему мы не должны создавать Spring MVC-контроллер Transactional
. См:
Итак, почему?
- Существуют ли непреодолимые технические проблемы?
- Есть ли архитектурные проблемы?
- Есть ли проблемы с производительностью/тупиком/ concurrency?
- Иногда требуется несколько отдельных транзакций? Если да, какие варианты использования? (Мне нравится упрощающий дизайн, который вызывает сервер либо полностью преуспевает, либо полностью терпит неудачу. Это звучит очень стабильно).
Фон:
Я работал несколько лет назад в команде на довольно большом ПО ERP, реализованном в С#/NHibernate/ Spring.Net. Поворот на сервер был точно реализован следующим образом: транзакция была открыта перед вводом любой логики контроллера и была завершена или отменена после выхода из контроллера. Сделка управлялась в рамках, чтобы никто не заботился об этом. Это было блестящее решение: стабильное, простое, только несколько архитекторов должны были заботиться о проблемах с транзакциями, остальная часть команды только что реализовала функции.
С моей точки зрения, это лучший дизайн, который я когда-либо видел. Поскольку я пытался воспроизвести тот же дизайн с помощью Spring MVC, я вступил в кошмар с проблемами ленивой загрузки и транзакций и каждый раз, когда тот же ответ: не делайте контроллер транзакционным, но почему?
Заранее благодарю за ваши обоснованные ответы!
Ответы
Ответ 1
TL;DR: это потому, что только уровень сервиса в приложении имеет логику, необходимую для определения области действия базы данных/бизнес-транзакции. Контроллер и уровень сохранения по дизайну не могут/не должны знать объем транзакции.
Контроллер можно сделать @Transactional
, но на самом деле это общая рекомендация только сделать транзакционный уровень сервиса (уровень сохранения не должен быть транзакционным).
Причиной этого является не техническая осуществимость, а разделение проблем. Ответственность диспетчера заключается в том, чтобы получить запросы параметров, а затем вызвать один или несколько методов службы и объединить результаты в ответе, который затем отправляется обратно клиенту.
Таким образом, контроллер имеет функцию координатора выполнения запроса и трансформатор данных домена в формат, который клиент может использовать, например, DTO.
Бизнес-логика находится на уровне сервиса, а уровень persistence просто извлекает/сохраняет данные взад и вперед из базы данных.
Объем транзакции базы данных на самом деле представляет собой концепцию бизнеса, а также техническую концепцию: при передаче учетной записи учетная запись может быть дебетована только в том случае, если другая кредитуется и т.д., поэтому только уровень обслуживания, который содержит бизнес-логику, может действительно знают объем транзакции по переводу банковских счетов.
Уровень сохранения не может знать, в какую транзакцию он входит, например, метод customerDao.saveAddress
. Должно ли оно запускать в нем собственную отдельную транзакцию? нет никакого способа узнать, это зависит от бизнес-логики, вызывающей его. Иногда он должен запускаться на отдельной транзакции, иногда только сохранять его данные, если также работал saveCustomer
и т.д.
То же самое относится к контроллеру: должны ли saveCustomer
и saveErrorMessages
идти в одной транзакции? Возможно, вы захотите сохранить клиента, и если это не сработает, попробуйте сохранить некоторые сообщения об ошибках и вернуть правильное сообщение об ошибке клиенту вместо того, чтобы откатывать все, включая сообщения об ошибках, которые вы хотели сохранить в базе данных.
В не-транзакционных контроллерах методы, возвращаемые из служебного уровня, возвращают отдельные объекты, поскольку сеанс закрыт. Это нормально, решение - либо использовать OpenSessionInView
, либо делать запросы, которые хотят получить результаты, которые контроллер знает, что им нужно.
Сказав, что это не преступление, чтобы сделать контролеры транзакционными, это просто не самая часто используемая практика.
Ответ 2
Я видел оба случая на практике, в веб-приложениях среднего и крупного бизнеса, используя различные веб-фреймворки (JSP/Struts 1.x, GWT, JSF 2, Java EE и Spring).
По моему опыту, лучше всего разграничить транзакции на самом высоком уровне, то есть на уровне "контроллера".
В одном случае у нас был класс BaseAction
, расширяющий класс Struts 'Action
, с реализацией метода execute(...)
, который обрабатывал управление сеансом Hibernate (сохранялся в объекте ThreadLocal
), начало/фиксация транзакции /rollback и отображение исключений для удобных сообщений об ошибках. Этот метод просто отменил бы текущую транзакцию, если бы какое-либо исключение распространилось до этого уровня или было отмечено только для отката; в противном случае он совершил транзакцию. Это работало в каждом случае, где обычно имеется одна транзакция базы данных для всего цикла HTTP-запроса/ответа. Редкие случаи, когда требуется несколько транзакций, будут обрабатываться в конкретном коде конкретного случая.
В случае GWT-RPC аналогичное решение было реализовано с помощью базовой реализации GWT Servlet.
В JSF 2 я до сих пор использовал только демаркацию уровня сервиса (с использованием сеанса EJB beans, который автоматически обрабатывал транзакцию "ТРЕБУЕТСЯ" ). Здесь есть недостатки, а не демаркетирование транзакций на уровне поддержки JSF beans. В основном проблема заключается в том, что во многих случаях контроллеру JSF необходимо совершать несколько служебных вызовов, каждый из которых обращается к базе данных приложений. При транзакциях на уровне обслуживания это подразумевает несколько отдельных транзакций (все они зафиксированы, если не возникает исключение), которые больше взимают с сервера базы данных. Однако это не просто недостаток производительности. Наличие нескольких транзакций для одного запроса/ответа также может привести к тонким ошибкам (я больше не помню детали, просто такие проблемы возникли).
Другой ответ на этот вопрос говорит о "логике, необходимой для определения сферы действия базы данных/бизнес-транзакции". Этот аргумент для меня не имеет смысла, поскольку обычно нет никакой логики, связанной с демаркацией транзакций. Ни классы контроллеров, ни классы обслуживания не должны "знать" о транзакциях. В подавляющем большинстве случаев в веб-приложении каждая бизнес-операция происходит внутри пары запросов/ответа HTTP, при этом объем транзакции представляет собой все отдельные операции, выполняемые с того момента, когда запрос получен до завершения ответа.
Иногда, бизнес-службе или контроллеру может потребоваться обработать исключение определенным образом, а затем, возможно, отметить текущую транзакцию только для отката. В Java EE (JTA) это делается путем вызова UserTransaction # setRollbackOnly(). Объект UserTransaction
может быть введен в поле @Resource
или получен программным путем из некоторого ThreadLocal
. В Spring аннотация @Transactional
позволяет указать откат для определенных типов исключений, или код может получить локальный поток TransactionStatus и вызовите setRollbackOnly()
.
Таким образом, по моему мнению и опыту, более эффективный подход к транзакциям контроллера.
Ответ 3
Иногда вы хотите отменить транзакцию при вызове исключения, но в то же время вы хотите обработать исключение, создайте для него правильный ответ в контроллере.
Если вы помещаете @Transactional
в метод контроллера единственный способ принудительно выполнить откат, он должен выкинуть транзакцию из метода контроллера, но тогда вы не сможете вернуть обычный объект ответа.
Обновление: Откат также может быть достигнут программно, как указано в Ответ Родерио.
Лучшее решение - сделать ваш метод сервиса транзакционным, а затем обработать возможное исключение в методах контроллера.
В следующем примере показана служба пользователя с помощью метода createUser
, этот метод отвечает за создание пользователя и отправку электронной почты пользователю. Если отправка почты не удалась, мы хотим отменить создание пользователя:
@Service
public class UserService {
@Transactional
public User createUser(Dto userDetails) {
// 1. create user and persist to DB
// 2. submit a confirmation mail
// -> might cause exception if mail server has an error
// return the user
}
}
Затем в вашем контроллере вы можете обернуть вызов createUser
в try/catch и создать правильный ответ пользователю:
@Controller
public class UserController {
@RequestMapping
public UserResultDto createUser (UserDto userDto) {
UserResultDto result = new UserResultDto();
try {
User user = userService.createUser(userDto);
// built result from user
} catch (Exception e) {
// transaction has already been rolled back.
result.message = "User could not be created " +
"because mail server caused error";
}
return result;
}
}
Если вы помещаете @Transaction
в свой метод контроллера, это просто невозможно.