Почему мы не должны делать Spring MVC controller @Transactional?


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

Так, почему?

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

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

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

спасибо заранее для ваших обоснованных ответов!

3   51   2014-04-16 23:46:12

3 ответа:

TLDR: это связано с тем, что только уровень сервиса в приложении имеет логику, необходимую для определения области базы данных/бизнес-транзакции. Контроллер и уровень сохраняемости по дизайну не могут / не должны знать область транзакции.

контроллер можно сделать @Transactional, но на самом деле это обычная рекомендация, чтобы только сделать уровень обслуживания транзакционным (уровень сохраняемости также не должен быть транзакционным).

причина это не техническая осуществимость, а разделение интересов. Ответственность контроллера заключается в получении запросов параметров, а затем вызове одного или нескольких методов службы и объединении результатов в ответ, который затем отправляется обратно клиенту.

таким образом, контроллер имеет функцию координатора выполнения запроса и преобразования данных домена в формат, который клиент может использовать, например, DTOs.

бизнес-логика находится на уровне сервиса, и уровень сохраняемости просто извлекает / сохраняет данные из базы данных.

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

уровень сохраняемости не может знать, в какой транзакции он находится, например метод customerDao.saveAddress. Должен ли он работать в своей собственной отдельной транзакции всегда? нет никакого способа узнать, это зависит от бизнес-логики, называющей его. Иногда он должен работать на отдельной транзакции, иногда только сохранить данные, если saveCustomer также работал и т. д.

то же самое относится и к контроллеру: должен saveCustomer и saveErrorMessages перейти в той же транзакции? Возможно, вы захотите сохранить клиента, и если это не удастся, попробуйте сохранить некоторые сообщения об ошибках и вернуть правильное сообщение об ошибке клиент вместо отката всего, включая сообщения об ошибках, которые вы хотели сохранить в базе данных.

в нетранзакционных контроллерах методы, возвращающиеся из уровня обслуживания, возвращают отсоединенные объекты, поскольку сеанс закрыт. Это нормально, решение либо использовать OpenSessionInViewили делать запросы, которые стремятся получить результаты контроллер знает, что он должен.

сказав это, это не преступление, чтобы сделать контроллеры транзакционными, это просто не самое часто используется практика.

Я видел оба случая на практике, в средних и крупных бизнес - веб-приложениях, используя различные веб-фреймворки (JSP/Struts 1.x, GWT, JSF 2, С Java EE и Spring).

по моему опыту, лучше всего разграничивать транзакции на самом высоком уровне, т. е. на уровне "контроллера".

в одном случае у нас был BaseAction класс, расширяющий распорки' Action класс, с реализации execute(...) метод, который обрабатывал управление сеансом гибернации (сохраненный в а ThreadLocal object), транзакция begin/commit/rollback и сопоставление исключений с удобными для пользователя сообщениями об ошибках. Этот метод просто откатит текущую транзакцию, если какое-либо исключение будет распространено до этого уровня или если оно было отмечено только для отката; в противном случае он зафиксирует транзакцию. Это работало в каждом случае, где обычно существует одна транзакция базы данных для всего цикла HTTP-запроса/ответа. Редкие случаи, когда требуется несколько транзакций, будут обрабатывается в конкретном коде прецедента.

в случае GWT-RPC аналогичное решение было реализовано с помощью базовой реализации сервлета GWT.

С JSF 2 я до сих пор использовал только демаркацию уровня обслуживания (используя сеансовые бобы EJB, которые автоматически имеют "необходимое" распространение транзакций). Здесь есть недостатки, в отличие от разграничения транзакций на уровне компонентов поддержки JSF. В принципе, проблема заключается в том, что во многих случаях контроллер JSF должен сделайте несколько вызовов службы, каждый из которых обращается к базе данных приложения. Для транзакций уровня обслуживания это означает несколько отдельных транзакций (все зафиксированные, если не возникает исключение), которые больше облагаются налогом на сервер базы данных. Однако это не просто недостаток производительности. Наличие нескольких транзакций для одного запроса / ответа также может привести к тонким ошибкам (я больше не помню деталей, просто такие проблемы возникали).

другой ответ на этот вопрос говорит о "логика, необходимая для определения области базы данных/бизнес-транзакции". Этот аргумент не имеет смысла для меня, поскольку нет логика, связанная с демаркацией транзакций вообще, обычно. Ни классы контроллеров, ни классы обслуживания не должны фактически "знать" о транзакциях. В подавляющем большинстве случаев в веб-приложении каждая бизнес-операция происходит внутри пары HTTP-запрос / ответ, причем область транзакции - это все отдельные операции выполняется с момента получения запроса до завершения ответа.

иногда бизнес-службе или контроллеру может потребоваться обработать исключение определенным образом, а затем, вероятно, отметить текущую транзакцию только для отката. В Java EE (JTA) это делается путем вызова UserTransaction#setRollbackOnly(). Элемент UserTransaction объект может быть введен в @Resource поле, или полученное программно от некоторых ThreadLocal. Весной, в @Transactional аннотация позволяет указать откат для определенных типов исключений, или код может получить поток-локальный TransactionStatus и звонок setRollbackOnly().

так что, по моему мнению и опыту, сделать контроллер транзакционным-это лучший подход.

иногда вы хотите откатить транзакцию при возникновении исключения, но в то же время вы хотите обработать исключение, чтобы создать правильный ответ в контроллере на него.

если поставить @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 на вашем методе контроллера это просто невозможно.