Por que não devemos fazer um controlador Spring MVC @Transactional?

Já existem algumas questões sobre o tópico, mas nenhuma resposta realmente fornece argumentos para explicar por que não devemos fazer um Transactional Spring MVC controller. Vejo:

Então por que?

  • Existem problemas técnicos insuperáveis ?
  • Existem problemas de arquitetura?
  • Há problemas de desempenho / impasse / simultaneidade?
  • Às vezes, várias transactions separadas são necessárias? Se sim, quais são os casos de uso? (Eu gosto do design simplificador, que as chamadas para o servidor são completamente bem-sucedidas ou falham completamente. Parece ser um comportamento muito estável)

Antecedentes: Eu trabalhei há alguns anos em uma equipe em um grande software ERP implementado em C # / NHibernate / Spring.Net. A ida e volta para o servidor foi exatamente implementada assim: a transação foi aberta antes de entrar em qualquer lógica do controlador e foi confirmada ou recuperada após sair do controlador. A transação foi gerenciada no framework para que ninguém tivesse que se preocupar com isso. Foi uma solução shiny: estável, simples, apenas alguns arquitetos tiveram que se preocupar com questões de transação, o resto da equipe apenas implementou resources.

Do meu ponto de vista, é o melhor design que já vi. Como eu tentei reproduzir o mesmo design com o Spring MVC, eu entrei em um pesadelo com problemas de carregamento lento e transação e toda vez a mesma resposta: não faça o controlador transacional, mas por quê?

Obrigado antecipadamente por suas respostas fundadas!

TLDR : isso ocorre porque somente a camada de serviço no aplicativo possui a lógica necessária para identificar o escopo de uma transação de database / negócios. O controlador e a camada de persistência por design não podem / não devem conhecer o escopo de uma transação.

O controlador pode ser transformado em @Transactional , mas na verdade é uma recomendação comum apenas tornar a camada de serviço transacional (a camada de persistência também não deve ser transacional).

A razão para isso não é viabilidade técnica, mas separação de preocupações. A responsabilidade do controlador é obter as solicitações de parâmetro e, em seguida, chamar um ou mais methods de serviço e combinar os resultados em uma resposta que é enviada de volta ao cliente.

Assim, o controlador tem uma function de coordenador da execução da solicitação e do transformador dos dados do domínio para um formato que o cliente possa consumir, como DTOs.

A lógica de negócios reside na camada de serviço e a camada de persistência apenas recupera / armazena dados do database.

O escopo de uma transação de database é realmente um conceito de negócio tanto quanto um conceito técnico: em uma transferência de conta, uma conta só pode ser debitada se a outra for creditada etc., portanto somente a camada de serviço que contém a lógica de negócios pode realmente conhecer o escopo de uma transação de transferência bancária.

A camada de persistência não pode saber em qual transação ela está, por exemplo, um método customerDao.saveAddress . Deve ser executado em sua própria transação separada sempre? não há como saber, depende da lógica comercial que o chama. Às vezes, ele deve ser executado em uma transação separada, às vezes, apenas salvar seus dados se o saveCustomer também funcionar, etc.

O mesmo se aplica ao controlador: saveCustomer e saveCustomer devem ir na mesma transação? Você pode querer salvar o cliente e se isso falhar, tente salvar algumas mensagens de erro e retornar uma mensagem de erro adequada ao cliente, em vez de reverter tudo, incluindo as mensagens de erro que você deseja salvar no database.

Em controladores não transacionais, os methods que retornam da camada de serviço retornam entidades desconectadas porque a session é fechada. Isso é normal, a solução é usar o OpenSessionInView ou fazer consultas que OpenSessionInView buscar os resultados que o controlador sabe que precisa.

Dito isto, não é um crime transformar os controladores em transacionais, não é apenas a prática mais usada.

Eu vi os dois casos na prática, em aplicações web de negócios de médio a grande porte, usando vários frameworks web (JSP / Struts 1.x, GWT, JSF 2, com Java EE e Spring).

Na minha experiência, é melhor demarcar as transactions no nível mais alto, ou seja, no nível “controlador”.

Em um caso, tínhamos uma class BaseAction estendendo a class Action Struts, com uma implementação para o método execute(...) que manipulava o gerenciamento de sessões do Hibernate (salvo em um object ThreadLocal ), transação begin / commit / rollback e o mapeamento de exceções a mensagens de erro fáceis de usar. Esse método simplesmente reverteria a transação atual se qualquer exceção fosse propagada até esse nível ou se estivesse marcada apenas para reversão; caso contrário, ele comprometeria a transação. Isso funcionou em todos os casos, onde normalmente há uma única transação de database para todo o ciclo de solicitação / resposta HTTP. Casos raros em que várias transactions eram necessárias seriam tratados no código específico do caso de uso.

No caso do GWT-RPC, uma solução semelhante foi implementada por uma implementação de Servlet GWT de base.

Com o JSF 2, até agora só usei a demarcação em nível de serviço (usando os beans de session EJB que têm automaticamente propagação de transação “REQUIRED”). Há desvantagens aqui, em oposição à demarcação de transactions no nível dos beans auxiliares do JSF. Basicamente, o problema é que, em muitos casos, o controlador JSF precisa fazer várias chamadas de serviço, cada uma acessando o database do aplicativo. Com transactions em nível de serviço, isso implica várias transactions separadas (todas confirmadas, a menos que ocorra uma exceção), que sobrecarrega mais o servidor de database. Não é apenas uma desvantagem de desempenho, no entanto. Ter várias transactions para uma única solicitação / resposta também pode levar a bugs sutis (não lembro mais dos detalhes, apenas que tais problemas ocorreram).

Outra resposta a esta pergunta refere-se à “lógica necessária para identificar o escopo de uma transação de database / negócios”. Esse argumento não faz sentido para mim, já que não lógica associada à demarcação de transação, normalmente. Nem as classs do controlador nem as classs de serviço precisam realmente “saber” sobre transactions. Na grande maioria dos casos, em um aplicativo da Web, cada operação de negócios ocorre dentro de um par solicitação / resposta HTTP, com o escopo da transação sendo todas as operações individuais sendo executadas a partir do ponto em que a solicitação é recebida até que a resposta seja concluída.

Ocasionalmente, um serviço de negócios ou controlador pode precisar manipular uma exceção de uma maneira específica e provavelmente marcar a transação atual apenas para reversão. No Java EE (JTA), isso é feito chamando UserTransaction # setRollbackOnly () . O object UserTransaction pode ser injetado em um campo @Resource ou obtido de forma programática a partir de algum ThreadLocal . No Spring, a anotação @Transactional permite que a reversão seja especificada para determinados tipos de exceção, ou o código pode obter um TransactionStatus thread-local e chamar setRollbackOnly() .

Então, na minha opinião e experiência, tornar o controlador transacional é a melhor abordagem.

Às vezes, você quer reverter uma transação quando uma exceção é lançada, mas ao mesmo tempo você deseja manipular a exceção e criar uma resposta adequada no controlador para ela.

Se você colocar @Transactional no método do controlador, a única maneira de forçar a reversão para lançar a transação do método do controlador, mas não poderá retornar um object de resposta normal.

Atualização: Um rollback também pode ser feito programaticamente, conforme descrito na resposta de Rodério .

Uma solução melhor é tornar seu método de serviço transacional e manipular uma possível exceção nos methods do controlador.

O exemplo a seguir mostra um serviço de usuário com um método createUser , esse método é responsável por criar o usuário e enviar um email para o usuário. Se o envio do email falhar, queremos reverter a criação do usuário:

 @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 } } 

Em seguida, em seu controlador, você pode envolver a chamada em createUser em uma tentativa / captura e criar uma resposta adequada para o usuário:

 @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; } } 

Se você colocar um @Transaction no seu método controlador, isso simplesmente não é possível.

Intereting Posts