Подтвердить что ты не робот

Почему мы не должны создавать Spring контроллер MVC @Transactional?

Уже есть несколько вопросов по теме, но никакой ответ вообще не дает аргументов, чтобы объяснить, почему мы не должны создавать Spring MVC-контроллер Transactional. См:

Итак, почему?

  • Существуют ли непреодолимые технические проблемы?
  • Есть ли архитектурные проблемы?
  • Есть ли проблемы с производительностью/тупиком/ concurrency?
  • Иногда требуется несколько отдельных транзакций? Если да, какие варианты использования? (Мне нравится упрощающий дизайн, который вызывает сервер либо полностью преуспевает, либо полностью терпит неудачу. Это звучит очень стабильно).

Фон: Я работал несколько лет назад в команде на довольно большом ПО ERP, реализованном в С#/NHibernate/ Spring.Net. Поворот на сервер был точно реализован следующим образом: транзакция была открыта перед вводом любой логики контроллера и была завершена или отменена после выхода из контроллера. Сделка управлялась в рамках, чтобы никто не заботился об этом. Это было блестящее решение: стабильное, простое, только несколько архитекторов должны были заботиться о проблемах с транзакциями, остальная часть команды только что реализовала функции.

С моей точки зрения, это лучший дизайн, который я когда-либо видел. Поскольку я пытался воспроизвести тот же дизайн с помощью Spring MVC, я вступил в кошмар с проблемами ленивой загрузки и транзакций и каждый раз, когда тот же ответ: не делайте контроллер транзакционным, но почему?

Заранее благодарю за ваши обоснованные ответы!

4b9b3361

Ответ 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 в свой метод контроллера, это просто невозможно.