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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23118789/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 20:51:16  来源:igfitidea点击:

Why we shouldn't make a Spring MVC controller @Transactional?

javaspring-mvccontrollertransactional

提问by jeromerg

There are already a few questions about the topic, but no response at all really provides arguments in order to explain why we shouldn't make a Spring MVC controller Transactional. See:

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

So, why?

所以为什么?

  • Is there insurmountabletechnical issues?
  • Is there architectural issues?
  • Is there performance/deadlock/concurrency issues?
  • Are sometimes multiple separate transactions required? If yes, what are the use cases? (I like the simplifying design, that calls to server either completely succeed or completely fail. It sounds to be a very stable behavior)
  • 是否存在无法克服的技术问题?
  • 是否存在架构问题?
  • 是否存在性能/死锁/并发问题?
  • 有时是否需要多个单独的事务?如果是,用例是什么?(我喜欢简化的设计,对服务器的调用要么完全成功,要么完全失败。这听起来是一种非常稳定的行为)

Background: I worked a few years ago in a Team on a quite large ERP Software implemented in C#/NHibernate/Spring.Net. The round-trip to the server was exactly implemented like that: the transaction was opened before entering any controller logic and was committed or rollbacked after exiting the controller. The transaction was managed in the framework so that no one had to care about it. It was a brilliant solution: stable, simple, only a few architects had to care about transaction issues, the rest of the team just implemented features.

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

From my point of view, it is the best design I have ever seen. As I tried to reproduce the same design with Spring MVC, I entered in a nightmare with lazy-loading and transaction issues and every time the same answer: don't make the controller transactional, but why?

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

Thank you in advance for your founded answers!

在此先感谢您的回答!

采纳答案by Angular University

TLDR: this is because only the service layer in the application has the logic needed to identify the scope of a database/business transaction. The controller and persistence layer by design can't/shouldn't know the scope of a transaction.

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

The controller can be made @Transactional, but indeed it's a common recommendation to only make the service layer transactional (the persistence layer should not be transactional either).

控制器可以被制作@Transactional,但实际上它是一个普遍的建议,只使服务层具有事务性(持久层也不应该是事务性的)。

The reason for this is not technical feasibility, but separation of concerns. The controller responsibility is to get the parameter requests, and then call one or more service methods and combine the results in a response that is then sent back to the client.

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

So the controller has a function of coordinator of the request execution, and transformer of the domain data to a format the client can consume such as DTOs.

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

The business logic resides on the service layer, and the persistence layer just retrieve / stores data back and forth from the database.

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

The scope of a database transaction is really a business concept as much as a technical concept: in an account transfer an account can only be debited if the other is credited etc., so only the service layer that contains the business logic can really know the scope of a bank account transfer transaction.

数据库事务的范围既是一个业务概念,也是一个技术概念:在一个账户转账中,一个账户只能借另一个账户贷记等,所以只有包含业务逻辑的服务层才能真正知道银行账户转账交易的范围。

The persistence layer cannot know what transaction it's in, take for example a method customerDao.saveAddress. Should it run in it's own separate transaction always? there is no way to know, it depends on the business logic calling it. Sometimes it should run on a separate transaction, sometimes only save it's data if the saveCustomeralso worked, etc.

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

The same applies to the controller: should saveCustomerand saveErrorMessagesgo in the same transaction? You might want to save the customer and if that fails then try to save some error messages and return a proper error message to the client, instead of rolling back everything including the error messages you wanted to save on the database.

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

In non transactional controllers, methods returning from the service layer return detached entities because the session is closed. This is normal, the solution is to either use OpenSessionInViewor do queries that eager fetch the results the controller knows it needs.

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

Having said that, it's not a crime to make controllers transactional, it's just not the most frequently used practice.

话虽如此,使控制器具有事务性并不是犯罪,只是不是最常用的做法。

回答by lanoxx

Sometimes you want to roll back a transaction when an exception is thrown, but at the same time you want to handle the exception create a proper response in the controller to it.

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

If you put @Transactionalon the controller method the only way to enforce the rollback it to throw the transaction from the controller method, but then you cannot return a normal response object.

如果您@Transactional使用控制器方法强制回滚它从控制器方法抛出事务的唯一方法,但是您无法返回正常的响应对象。

Update:A rollback can also be achieved programmatically, as outlined in Rodério's answer.

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

A better solution is to make your service method transactional and then handle a possible exception in the controller methods.

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

The following example shows a user service with a createUsermethod, that method is responsible to create the user and send an email to the user. If sending the mail fails we want to rollback the user creation:

下面的示例展示了一个带有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
    }
}

Then in your controller you can wrap the call to createUserin a try/catch and create a proper response to 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;
    }
}

If you put a @Transactionon your controller method that is simply not possible.

如果你@Transaction在你的控制器方法上放了一个,那是不可能的。

回答by Rogério

I have seen both cases in practice, in medium- to large-sized business web applications, using various web frameworks (JSP/Struts 1.x, GWT, JSF 2, with Java EE and Spring).

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

In my experience, it's best to demarcate transactions at the highest level, ie, at the "controller" level.

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

In one case, we had a BaseActionclass extending Struts' Actionclass, with an implementation for the execute(...)method that handled Hibernate session management (saved into a ThreadLocalobject), transaction begin/commit/rollback, and the mapping of exceptions to user-friendly error messages. This method would simply rollback the current transaction if any exception got propagated up to this level, or if it was marked for rollback only; otherwise, it would commit the transaction. This worked in every case, where normally there is a single database transaction for the whole HTTP request/response cycle. Rare cases where multiple transactions were needed would be handled in use-case specific code.

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

In the case of GWT-RPC, a similar solution was implemented by a base GWT Servlet implementation.

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

With JSF 2, I've so far only used service-level demarcation (using EJB Session beans which automatically have "REQUIRED" transaction propagation). There are disadvantages here, as opposed to demarcating transactions at the level of the JSF backing beans. Basically, the problem is that in many cases the JSF controller needs to make several service calls, each one accessing the application database. With service-level transactions, this implies several separate transactions (all committed, unless an exception occurs), which taxes more the database server. It isn't just a performance disadvantage, though. Having multiple transactions for a single request/response can also lead to subtle bugs (I don't remember the details anymore, just that such issues did occur).

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

Other answer to this question talks about "logic needed to identify the scope of a database/business transaction". This argument doesn't make sense to me, since there is nologic associated with transaction demarcation at all, normally. Neither controller classes nor service classes need to actually "know" about transactions. In the vast majority of cases, in a web app each business operation occurs inside an HTTP request/response pair, with the scope of the transaction being all the individual operations being executed from the point the request is received up until the response being finished.

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

Occasionaly, a business service or controller may need to handle an exception in a particular way, then probably mark the current transaction for rollback only. In Java EE (JTA), this is done by calling UserTransaction#setRollbackOnly(). The UserTransactionobject can be injected into a @Resourcefield, or obtained programmatically from some ThreadLocal. In Spring, the @Transactionalannotation allows rollback to be specified for certain exception types, or code can obtain a thread-local TransactionStatusand call setRollbackOnly().

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

So, in my opinion and experience, making the controller transactional is the better approach.

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