为什么我们不能创建一个 Spring MVC 控制器@Transactional?

已经有一些关于这个主题的问题,但是没有任何回应真正提供参数来解释为什么我们不应该使用 Spring MVC 控制器 Transactional。参见:

为什么?

  • 无法逾越有技术问题吗?
  • 有建筑方面的问题吗?
  • 是否存在性能/死锁/并发性问题?
  • 有时需要多个单独的事务吗?如果是,用例是什么?(我喜欢简化的设计,调用服务器要么完全成功,要么完全失败。这听起来是一种非常稳定的行为)

背景: 几年前,我曾在一个团队中工作,负责一个用 C #/NHibernate/Spring 实现的大型 ERP 软件。网。到服务器的往返就是这样实现的: 事务在进入任何控制器逻辑之前打开,在退出控制器之后提交或回滚。事务是在框架中管理的,因此没有人必须关心它。这是一个绝妙的解决方案: 稳定、简单,只有少数架构师需要关心事务问题,团队中的其他人只需要实现特性。

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

提前感谢您的有根据的回答!

51219 次浏览

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

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

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

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

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

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

持久层不能知道它在什么事务中,例如 customerDao.saveAddress方法。它应该总是在它自己的单独事务中运行吗?没有办法知道,这取决于调用它的业务逻辑。有时它应该运行在一个单独的事务上,有时只保存它的数据,如果 saveCustomer也工作,等等。

同样的情况也适用于控制器: saveCustomersaveErrorMessages是否应该进入同一个事务?您可能希望保存客户,如果这样做失败了,那么尝试保存一些错误消息并向客户端返回正确的错误消息,而不是回滚所有内容,包括您想要保存在数据库中的错误消息。

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

话虽如此,让控制者进行交易并不是犯罪,只是这不是最常用的做法。

有时,您希望在抛出异常时回滚事务,但同时又希望处理该异常,从而在控制器中对该异常创建适当的响应。

如果将 @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
}
}

然后,在你的控制器中,你可以用 try/catch 来包装对 createUser的调用,并为用户创建一个正确的响应:

@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,那是不可能的。

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

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

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

对于 GWT-RPC,类似的解决方案是通过基本的 GWT Servlet 实现实现的。

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

对这个问题的其他回答涉及“识别数据库/业务事务范围所需的逻辑”。这个参数对我来说没有意义,因为通常有 没有逻辑与事务划分相关联。控制器类和服务类都不需要实际“知道”事务。在绝大多数情况下,在一个 web 应用中,每个业务操作都发生在一个 HTTP 请求/响应对中,事务的范围是从接收请求到响应完成之间执行的所有单个操作。

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

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

我认为最好的方法是将事务注释放在从实体(持久对象)转换到 DTO (瞬态对象)的层中。这样做的原因是,可能需要遍历实体关系,这可能会触发延迟初始化,因此不希望使用 视图中的开放会话反模式

将它们放在控制器级别的一个参数是,REST 已经规定了事务的只读或读写属性。(GET 应该是只读的,POST/PUT/DELETE 应该是读写的)。请注意,这只有在错误响应处理异常发生在控制器之外时才会有效,因此当发生这种情况时,事务将被正确地回滚。

将它们放在服务层的一个理由是,服务可能依赖于其内部工作的正确事务隔离/传播。

也许最好的折衷方案是设计外观级别的服务,这些服务只在 API 中列出 DTO 对象,并将事务注释作为其接口的一部分。在设计接口时要考虑事务处理,这一点很重要,因为从只读方法调用读写方法可能具有 不良副作用