【问题标题】:Why we shouldn't make a Spring MVC controller @Transactional?为什么我们不应该创建一个 Spring MVC 控制器@Transactional?
【发布时间】:2014-04-16 19:46:12
【问题描述】:

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

那么,为什么?

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

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

在我看来,这是我见过的最好的设计。当我尝试使用 Spring MVC 重现相同的设计时,我陷入了延迟加载和事务问题的噩梦,并且每次都得到相同的答案:不要让控制器事务性,但是为什么?

提前感谢您提供有根据的答案!

【问题讨论】:

    标签: java spring-mvc controller transactional


    【解决方案1】:

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

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

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

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

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

    数据库事务的范围实际上是一个业务概念,同时也是一个技术概念:在一个账户转账中,只有在另一个账户被贷记的情况下,一个账户才能被借记等,所以只有包含业务逻辑的服务层才能真正了解银行账户转账交易的范围。

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

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

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

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

    【讨论】:

    • 谢谢! 1) 你说:持久层也不应该是事务性的。你到底是什么意思? Spring 文档:存储库实例上的 CRUD 方法默认是事务性的。 2) 你说:控制器的职责是将结果组合成响应。好的,但是出于这个目的,您需要跨服务调用执行业务逻辑和转换的事务。 3) “Eager fetching”不是从服务到控制器的依赖倒置吗?
    • '默认情况下,存储库实例上的 CRUD 方法是事务性的'意味着默认情况下它们在传播 REQUIRED 中运行,这意味着它们加入正在进行的事务,如果没有,则在它们自己的事务中运行。
    • '您需要跨服务调用执行业务逻辑和转换的跨界事务' - 如果它是跨界事务,那么它可能应该在服务层的业务方法中,因为它可以是在应用程序的其他地方重用。如果它在控制器中,那么它将是一个只能通过 HTTP 调用的业务方法,这不是完全错误的,只是不是最常见的设计
    • Eager fetching" 不是从服务到控制器的依赖倒置? - 对此没有明确的答案,关于该问题的帖子通常基于意见而关闭。看看我在评论中的博客文章以上关于通常的利弊,确实你有一点,但是在视图中使用开放会话等替代方法也可能有一些陷阱
    • 但是您提出的使控制器具有事务性的设计在许多情况下可能都非常好,如果它可以通过各种方式简化您的生活,那么就可以使用它,没有技术限制,控制器可以像任何控制器一样具有事务性其他豆子。希望对你有帮助
    【解决方案2】:

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

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

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

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

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

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

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

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

    【讨论】:

    • 按照以下讨论中的承诺奖励 100 赏金,感谢您分享您的观点。
    • @Rogerio 在控制器方法上使用事务注释为控制器类创建代理对象?如果我们使用 Transactional 注释,为什么它会生成代理对象。据我所知,创建代理是为了在运行时添加一些额外的功能,如果是这样,事务注释添加到控制器方法的额外功能是什么?
    • "在应用需要为控制器对象创建代理的功能(例如@Transactional 方法)时,使用带注释的控制器类时会发生一个常见的陷阱。通常你会为控制器引入一个接口为了使用 JDK 动态代理。要完成这项工作,您必须将`@RequestMapping` 注释以及任何其他类型和方法级别的注释(例如@ModelAttribute、@InitBinder)移动到接口以及映射机制只能“看到”代理暴露的接口”。将 spring mvc doc 中的参考添加到上述疑问
    【解决方案3】:

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

    如果您将@Transactional 放在控制器方法上,这是强制回滚的唯一方法,它会从控制器方法中抛出事务,但是您无法返回正常的响应对象。

    更新:也可以通过编程实现回滚,如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
        }
    }
    

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

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

    【讨论】:

    • 没有。在 Java EE 和 Spring 中,当前事务都可以通过编程方式标记为“仅回滚”。因此,您可以处理异常并产生“正常”结果,同时确保事务从@Transactional 控制器回滚。此外,为了提供特定的错误消息而捕获异常是一个糟糕的解决方案。异常到错误消息的映射应该在一个中心位置完成一次。在架构良好的应用程序中,任何自定义异常处理都应该是非常少见的。
    • 您能否在单独的答案中详细说明?
    • 我可以,但事实上已经有一个公认的答案(顺便说一句,我不同意),因此不值得(相当大的)努力来写一个详细的答案。
    • 我可以提供 100 声望的赏金,如果这能改变你的想法。
    • @Rogério 我同意集中处理错误,但是对于应用程序的不同部分的不同配置几乎没有支持,例如:公共部分对同一异常有更通用的消息。所以有时我不得不使用自定义处理。有什么建议可以避免吗?
    【解决方案4】:

    我认为最好的方法是将事务注释放在从实体(持久对象)过渡到 DTO(瞬态对象)的层。原因是可能需要遍历实体关系,这可能会触发延迟初始化,并且您不想使用open session in view anti-pattern

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

    将它们放在服务层的一个论据是,服务可能依赖于适当的事务隔离/传播来实现其内部工作。

    可能最好的折衷方案是设计外观级服务,该服务仅在其 API 中列出 DTO 对象,并将事务注释作为其接口的一部分。考虑到事务处理设计接口很重要,因为从只读方法调用读写方法可能有undesirable side effects

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-12-02
      • 1970-01-01
      • 2012-11-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-01
      相关资源
      最近更新 更多