为什么我们不应该创build一个Spring MVC控制器@Transactional?
关于这个话题已经有几个问题了,但是没有任何回应真的提供了论据来解释为什么我们不应该使Spring MVC控制器成为Transactional
。 看到:
-
事务不能正常工作 – Spring / MyBatis
-
对于Web MVC Spring应用程序应该@Transactional去控制器或服务?
-
制作Spring 3 MVC控制器方法Transactional
-
Spring MVC控制器事务
所以为什么?
- 有不可克服的技术问题吗?
- 有build筑问题吗?
- 是否有性能/死锁/并发问题?
- 有时需要多个独立的交易? 如果是,那么用例是什么? (我喜欢简化的devise,调用服务器要么完全成功,要么完全失败,这听起来是一个非常稳定的行为)
背景:几年前,我在一个团队中工作了一个在C#/ NHibernate / Spring.Net中实现的相当庞大的ERP软件。 服务器的往返是这样实现的:事务在进入任何控制器逻辑之前被打开,并在退出控制器之后被提交或回滚。 交易是在框架内pipe理的,所以没有人需要关心它。 这是一个绝妙的解决scheme:稳定,简单,只有less数架构师不得不关心交易问题,其余的团队只是实现了function。
从我的angular度来看,这是我所见过的最好的devise。 当我尝试用Spring MVC重现相同的devise时,我进入了懒惰加载和事务问题的噩梦,每次都有相同的答案:不要使控制器事务,但为什么?
预先感谢您的build议答案!
TLDR :这是因为只有应用程序中的服务层具有识别数据库/业务事务范围所需的逻辑。 devise中的控制器和持久层不能/不应该知道事务的范围。
控制器可以被创build@Transactional
,但事实上这是一个常见的build议,只是使服务层事务性(持久层也不应该是事务性的)。
原因不是技术上的可行性,而是关注点的分离。 控制器的责任是获取参数请求,然后调用一个或多个服务方法,并将结果合并到一个响应中,然后将其发送回客户端。
因此,控制器具有请求执行的协调器的function,并且域数据的变换器以客户机能够使用的格式(诸如DTO)进行转换。
业务逻辑驻留在服务层上,持久层只是从数据库中来回取回/存储数据。
数据库事务的范围和技术概念一样是一个真正的业务概念:在一个账户转移中,一个账户只有在另一个账户被记入账户时才能被扣除,所以只有包含业务逻辑的服务层才能真正知道银行账户转账交易的范围。
持久层无法知道它在哪个事务,例如一个方法customerDao.saveAddress
。 它应该总是在自己的独立交易中运行吗? 没有办法知道,这取决于调用它的业务逻辑。 有时它应该在单独的事务上运行,如果saveCustomer
也起作用,有时候只保存它的数据。
同样适用于控制器: saveCustomer
和saveErrorMessages
应该在同一个事务中吗? 您可能想要保存客户,如果失败,则尝试保存一些错误消息并向客户端返回适当的错误消息,而不是回滚所有包括要保存在数据库上的错误消息。
在非事务控制器中,从服务层返回的方法返回分离的实体,因为会话是closures的。 这是正常的,解决scheme是使用OpenSessionInView
或做渴望获取控制器知道它需要的结果的查询。
话虽如此,控制器交易并不是犯罪行为,但它并不是最常用的做法。
我已经在实践中看到了这两种情况,在大中型商业Web应用程序中,使用各种Web框架(JSP / Struts 1.x,GWT,JSF 2,Java EE和Spring)。
根据我的经验,最好是划定最高级别的交易,即“控制器”级别的交易。
在一个例子中,我们有一个扩展了Struts的Action
类的BaseAction
类,并且执行了处理Hibernate会话pipe理(保存到ThreadLocal
对象中)的execute(...)
方法,transaction begin / commit / rollback和mapping对用户友好的错误消息的例外。 如果有任何exception被传播到这个级别,或者它被标记为仅回退,则此方法将简单地回滚当前事务。 否则,它会提交交易。 这在任何情况下都是有效的,通常在整个HTTP请求/响应周期中有一个数据库事务。 在需要多个事务的罕见情况下,将在特定于用例的代码中处理。
在GWT-RPC的情况下,类似的解决scheme是通过基础的GWT Servlet实现来实现的。
使用JSF 2,到目前为止,我只使用服务级分界(使用自动具有“必需”事务传播的EJB会话bean)。 这里有一些缺点,而不是在JSF支持bean级别划分事务。 基本上,问题是在许多情况下,JSF控制器需要进行多个服务调用,每个调用都访问应用程序数据库。 对于服务级别的事务,这意味着几个单独的事务(全部承诺,除非发生exception),这个事务向数据库服务器征收更多的税。 不过,这不仅仅是一个性能劣势。 对于单个请求/响应有多个事务也可能导致微妙的错误(我不记得细节了,只是发生了这样的问题)。
对这个问题的其他答案谈到“确定数据库/业务交易范围所需的逻辑”。 这个论点对我来说是没有意义的,因为通常根本不存在与交易分界相关的逻辑。 控制器类和服务类都不需要真正“知道”事务。 在绝大多数情况下,在Web应用程序中,每个业务操作都发生在HTTP请求/响应对中,事务的范围是从请求被接收的点开始执行的所有单独的操作,直到响应被完成为止。
偶尔,业务服务或控制器可能需要以特定的方式处理exception,那么可能只标记当前的事务回滚。 在Java EE(JTA)中,这通过调用UserTransaction#setRollbackOnly()来完成 。 UserTransaction
对象可以被注入到一个@Resource
域中,或者从一些ThreadLocal
编程方式获得。 在Spring中,@ Transactional批注允许为某些exceptiontypes指定回滚,或者代码可以获得线程本地TransactionStatus并调用setRollbackOnly()
。
所以,根据我的观点和经验,使控制器交易是更好的方法。
有时候,当抛出一个exception时,你想要回滚一个事务,但是同时你想处理这个exception,在控制器中创build一个正确的响应。
如果您将 @Transactional
放在控制器方法上,则只有强制执行回滚才能从控制器方法中抛出事务,但是不能返回正常的响应对象。
更新:如Rodério的回答所述,还可以通过编程实现回滚。
更好的解决scheme是使你的服务方法事务化,然后在控制器方法中处理一个可能的exception。
以下示例显示了具有createUser
方法的用户服务,该方法负责创build用户并向用户发送电子邮件。 如果发送邮件失败,我们要回滚用户创build:
@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 } }
然后在您的控制器中,您可以将调用包装到try / catch中的createUser
,并为用户创build适当的响应:
@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
放在你的控制器方法上,那根本是不可能的。