为什么我们不应该@Transactional制作Spring MVC控制器?

2022-08-31 12:30:19

关于这个话题已经有几个问题,但是根本没有任何回应真正提供论据来解释为什么我们不应该制作Spring MVC控制器 。看:Transactional

那么,为什么呢?

  • 是否存在无法克服的技术问题?
  • 是否存在体系结构问题?
  • 是否存在性能/死锁/并发问题?
  • 有时是否需要多个单独的事务?如果是,有哪些用例?(我喜欢简化的设计,对服务器的调用要么完全成功,要么完全失败。这听起来像是一个非常稳定的行为)

背景:几年前,我在一个团队中工作,开发了一个相当大的ERP软件,该软件在C#/NHibernate/Spring.Net中实现。到服务器的往返行程就是这样实现的:事务在进入任何控制器逻辑之前打开,并在退出控制器后提交或回滚。交易在框架中进行管理,因此没有人需要关心它。这是一个出色的解决方案:稳定,简单,只有少数架构师必须关心交易问题,团队的其他成员只是实现了功能。

从我的角度来看,这是我见过的最好的设计。当我试图用Spring MVC重现相同的设计时,我陷入了一场噩梦,有延迟加载和事务问题,每次都有相同的答案:不要让控制器成为事务性的,但为什么?

提前感谢您的回答!


答案 1

TLDR:这是因为只有应用程序中的服务层具有标识数据库/业务事务范围所需的逻辑。控制器和持久层在设计上不能/不应该知道事务的范围。

控制器是可以制造的,但实际上,通常建议只使服务层成为事务性的(持久性层也不应该是事务性的)。@Transactional

其原因不是技术可行性,而是关注点的分离。控制器的职责是获取参数请求,然后调用一个或多个服务方法,并将结果合并到响应中,然后将其发送回客户端。

因此,控制器具有协调请求执行的功能,并将域数据转换为客户端可以使用的格式(如 DTO)。

业务逻辑驻留在服务层,持久层只是从数据库中来回检索/存储数据。

数据库交易的范围实际上是一个商业概念,也是一个技术概念:在账户转账中,只有当另一个账户被记入贷方等时,才能借记一个账户,因此只有包含业务逻辑的服务层才能真正知道银行账户转账交易的范围。

持久层无法知道它在哪个事务中,例如方法。它应该始终在自己的单独事务中运行吗?没有办法知道,这取决于调用它的业务逻辑。有时它应该在单独的事务上运行,有时只有在也有效的情况下保存它的数据,等等。customerDao.saveAddresssaveCustomer

这同样适用于控制器:应该和进入同一笔交易?您可能希望保存客户,如果失败,请尝试保存一些错误消息并向客户端返回正确的错误消息,而不是回滚所有内容,包括要保存在数据库中的错误消息。saveCustomersaveErrorMessages

在非事务控制器中,从服务层返回的方法返回分离的实体,因为会话已关闭。这是正常的,解决方案是使用或执行查询,这些查询渴望获取控制器知道它需要的结果。OpenSessionInView

话虽如此,使控制器成为交易并不是犯罪,它只是不是最常用的做法。


答案 2

我在实践中看到了这两种情况,在大中型商业Web应用程序中,使用各种Web框架(JSP / Struts 1.x,GWT,JSF 2,Java EE和Spring)。

根据我的经验,最好在最高级别(即“控制器”级别)划分交易。

在一个案例中,我们有一个扩展 Struts 类的类,其方法的实现处理 Hibernate 会话管理(保存到对象中)、事务开始/提交/回滚以及异常映射到用户友好的错误消息。如果任何异常传播到此级别,或者如果它被标记为仅回滚,则此方法将简单地回滚当前事务;否则,它将提交事务。这适用于每种情况下,通常整个HTTP请求/响应周期都有一个数据库事务。在极少数情况下,需要多个事务将在特定于用例的代码中处理。BaseActionActionexecute(...)ThreadLocal

在GWT-RPC的情况下,类似的解决方案是由基本的GWT Servlet实现的。

在 JSF 2 中,到目前为止,我只使用了服务级别划分(使用自动具有“必需”事务传播的 EJB 会话 Bean)。这里存在缺点,而不是在 JSF 支持 Bean 级别划分事务。基本上,问题在于在许多情况下,JSF 控制器需要进行多个服务调用,每个调用都访问应用程序数据库。对于服务级别事务,这意味着几个单独的事务(除非发生异常,否则全部提交),这会给数据库服务器带来更多负担。不过,这不仅仅是性能劣势。为单个请求/响应进行多个事务也可能导致微妙的错误(我不记得细节了,只是这些问题确实发生了)。

这个问题的其他答案是关于“识别数据库/业务事务范围所需的逻辑”。这个论点对我来说没有意义,因为通常根本没有与事务划分相关的逻辑。控制器类和服务类都不需要实际“了解”事务。在绝大多数情况下,在 Web 应用中,每个业务操作都发生在 HTTP 请求/响应对内,事务的范围是从接收请求点到响应完成为止执行的所有单个操作。

有时,业务服务或控制器可能需要以特定方式处理异常,然后可能仅将当前事务标记为回滚。在Java EE(JTA)中,这是通过调用UserTransaction#setRollbackOnly()来完成的。该对象可以注入到字段中,也可以以编程方式从某些 .在Spring中,注释允许为某些异常类型指定回滚,或者代码可以获取线程本地TransactStatus并调用。UserTransaction@ResourceThreadLocal@TransactionalsetRollbackOnly()

因此,根据我的观点和经验,使控制器事务性是更好的方法。