简体   繁体   English

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

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

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 .已经有一些关于该主题的问题,但根本没有任何回应真正提供 arguments 来解释为什么我们不应该制作 Spring MVC controller Transactional See:看:

So, why?所以为什么?

  • Is there insurmountable technical 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.背景:几年前,我在一个团队中工作,开发了一个用 C#/NHibernate/Spring.Net 实现的相当大的 ERP 软件。 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.到服务器的往返就是这样实现的:在进入任何controller逻辑之前打开事务,在退出controller逻辑之后提交或回滚。事务在框架中进行管理,因此无需任何人关心。 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.这是一个绝妙的解决方案:稳定、简单,只有少数架构师需要关心事务问题,团队的 rest 刚刚实现了功能。

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 重现相同的设计时,我陷入了延迟加载和事务问题的噩梦中,每次都是相同的答案:不要让 controller 成为事务性的,但为什么呢?

Thank you in advance for your founded answers!预先感谢您提供有根据的答案!

TLDR : this is because only the service layer in the application has the logic needed to identify the scope of a database/business transaction. TLDR :这是因为只有应用程序中的服务层具有识别数据库/业务事务范围所需的逻辑。 The controller and persistence layer by design can't/shouldn't know the scope of a transaction. 设计的控制器和持久层不能/不应该知道事务的范围。

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 . 持久层无法知道它所处的事务,例如使用方法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 saveCustomer also worked, etc. 有时候它应该在一个单独的事务上运行,有时只在saveCustomer工作时才保存它的数据等。

The same applies to the controller: should saveCustomer and saveErrorMessages go in the same transaction? 这同样适用于控制器: saveCustomersaveErrorMessages应该在同一个事务中吗? 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. 您可能希望保存客户,如果失败,则尝试保存一些错误消息并向客户端返回正确的错误消息,而不是回滚包括您要保存在数据库上的错误消息的所有内容。

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 OpenSessionInView or 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. 话虽如此,控制器交易并不是犯罪行为,但这并不是最常用的做法。

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 BaseAction class extending Struts' Action class, with an implementation for the execute(...) method that handled Hibernate session management (saved into a ThreadLocal object), transaction begin/commit/rollback, and the mapping of exceptions to user-friendly error messages. 在一种情况下,我们有一个BaseAction类扩展Struts的Action类,其中包含一个execute(...)方法的实现,该方法处理Hibernate会话管理(保存到ThreadLocal对象中),事务begin / commit / rollback和映射用户友好的错误消息的例外情况。 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. 这在每种情况下都有效,通常整个HTTP请求/响应周期都有一个数据库事务。 Rare cases where multiple transactions were needed would be handled in use-case specific code. 需要多个事务的罕见情况将在特定于用例的代码中处理。

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). 使用JSF 2,到目前为止我只使用了服务级别划分(使用自动具有“必需”事务传播的EJB会话bean)。 There are disadvantages here, as opposed to demarcating transactions at the level of the JSF backing beans. 这里有一些缺点,而不是在JSF支持bean级别划分事务。 Basically, the problem is that in many cases the JSF controller needs to make several service calls, each one accessing the application database. 基本上,问题在于,在许多情况下,JSF控制器需要进行多次服务调用,每次调用都访问应用程序数据库。 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). 单个请求/响应具有多个事务也可能导致细微的错误(我不再记得细节,只是发生了这样的问题)。

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 no logic 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() . 在Java EE(JTA)中,这是通过调用UserTransaction#setRollbackOnly()来完成的 The UserTransaction object can be injected into a @Resource field, or obtained programmatically from some ThreadLocal . UserTransaction对象可以注入@Resource字段,也可以从某些ThreadLocal编程方式获得。 In Spring, the @Transactional annotation allows rollback to be specified for certain exception types, or code can obtain a thread-local TransactionStatus and call setRollbackOnly() . 在春天, @Transactional注释允许某些例外类型指定回滚,或代码可以得到一个线程本地的TransactionStatus和呼叫setRollbackOnly()

So, in my opinion and experience, making the controller transactional is the better approach. 因此,根据我的观点和经验,使控制器事务处理是更好的方法。

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 @Transactional on 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的回答所述,也可以通过编程方式实现回滚。

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 createUser method, that method is responsible to create the user and send an email to the user. 以下示例显示了具有createUser方法的用户服务,该方法负责创建用户并向用户发送电子邮件。 If sending the mail fails we want to rollback the user creation: 如果发送邮件失败,我们想要回滚用户创建:

@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 createUser in a try/catch and create a proper response to the user: 然后在您的控制器中,您可以在try / catch中包含对createUser的调用, 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;
    }
}

If you put a @Transaction on your controller method that is simply not possible. 如果你在控制器方法上放置一个根本不可能的@Transaction

I think the best way is to put the transactional annotation at the layer where you transition from entities (persistent objects) to DTOs (transient objects).我认为最好的方法是将事务注释放在从实体(持久对象)转换到 DTO(瞬态对象)的层。 The reason for this is that entity relations may need to be traversed which may trigger lazy initializations and you don't want to use the open session in view anti-pattern .这样做的原因是可能需要遍历实体关系,这可能会触发延迟初始化,并且您不想使用open session in view anti-pattern

One argument to put them at the controller level would be that REST already dictates the read-only or read-write properties of a transaction.将它们置于 controller 级别的一个论据是 REST 已经规定了事务的只读或读写属性。 (GET should be read-only and POST/PUT/DELETE should be read-write). (GET 应该是只读的,POST/PUT/DELETE 应该是可读写的)。 Mind that this would only work if your exception to error response handling happens outside of the controller so a transaction is properly rolled back when this happens.请注意,只有当您的错误响应处理异常发生在 controller 之外时,这才会起作用,因此在发生这种情况时事务会被正确回滚。

An argument to put them at the service layer is that the service probably relies on the proper transaction isolation/propagation for its inner workings.将它们放在服务层的一个论点是服务可能依赖于其内部工作的适当事务隔离/传播。

Probably the best compromise is to design facade level services which only list DTO object in their API and contain the transaction annotations as part of their interface.可能最好的折衷方案是设计外观级服务,该服务仅在其 API 中列出 DTO object 并将事务注释作为其接口的一部分。 It is important to design the interfaces with the transactional handling in mind, since calling a read-write method from a read-only method may have undesirable side effects .在设计接口时考虑事务处理很重要,因为从只读方法调用读写方法可能会产生不良副作用

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM