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

时间:2014-04-16 19:46:13

标签: java spring-mvc controller transactional

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

那么,为什么?

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

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

从我的角度来看,这是我见过的最好的设计。当我尝试使用Spring MVC重现相同的设计时,我进入了一个关于延迟加载和事务问题的噩梦,并且每次都有相同的答案:不要让控制器进行事务处理,而是为什么?

提前感谢您的答案!

3 个答案:

答案 0 :(得分:91)

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

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

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

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

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

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

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

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

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

话虽如此,控制器交易并不是犯罪行为,但这并不是最常用的做法。

答案 1 :(得分:17)

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

根据我的经验,最好在最高级别划分交易,即在"控制器"水平。

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

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

使用JSF 2,我到目前为止只使用了服务级别划分(使用EJB会话bean自动拥有" REQUIRED"事务传播)。这里有一些缺点,而不是在JSF支持bean级别划分事务。基本上,问题在于,在许多情况下,JSF控制器需要进行多次服务调用,每次调用都访问应用程序数据库。对于服务级别的事务,这意味着几个单独的事务(所有提交,除非发生异常),这会对数据库服务器征税。但这并不仅仅是性能上的劣势。单个请求/响应的多个事务也可能导致微妙的错误(我不再记得细节,只是确实发生了这样的问题)。

此问题的其他答案谈及识别数据库/业务交易范围所需的逻辑"。这个论点对我来说没有意义,因为通常存在与事务划分相关联的 no 逻辑。控制器类和服务类都不需要实际地知道"关于交易。在绝大多数情况下,在Web应用程序中,每个业务操作都发生在HTTP请求/响应对中,事务的范围是从接收请求到响应完成之间执行的所有单独操作。

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

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

答案 2 :(得分:5)

有时你想在抛出异常时回滚一个事务,但是在你想要处理异常的同时在控制器中为它创建一个正确的响应。

如果你在控制器方法上放置@Transactional是强制执行回滚的唯一方法,就是从控制器方法中抛出事务,但是你不能返回正常的响应对象。 < / p>

更新:还可以通过编程方式实现回滚,如Rodério's answer中所述。

更好的解决方案是使您的服务方法具有事务性,然后在控制器方法中处理可能的异常。

以下示例显示了使用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,那是不可能的。