【问题标题】:How does Java EE container control transactions?Java EE 容器如何控制事务?
【发布时间】:2013-12-21 17:42:01
【问题描述】:

我有一个关于 EE 容器如何控制事务的问题。这是为我的问题提供一些上下文的伪代码。这不是我编码的方式,所以请留在问题上,不要将主题演变为其他内容。

考虑以下两个服务和相关的控制器。这两个服务都注入了 EntityManager 并且都有需要在事务中运行的方法。一项服务具有不需要任何事务支持的方法。

@Stateless
class UserService {
    @PersistenceContext private EntityManager em;

    public void saveUser(User user) {
        em.merge(user);
    }

    public String getFullName(User user) {
        return user.getFirstName() + " " + user.getLastName();
    }
}

@Stateless
class LogService {
    @PersistenceContext private EntityManager em;

    public void logEvent(String eventText) {
        Event event=new Event();
        event.setText(eventText);
        event.setTime(new Date());
        em.persist(event);
    }
}


@Named
class UserController {
    User user;

    @Inject UserService userService;
    @Inject LogService logService;

    public void updateUser(user) { // button posts to this method
        String fullName=userService.getFullName(user);  // 1
        if(fullName.startsWith("X")) return;            // 2
        userService.saveUser(user);                     // 3
        logService.logEvent("Saved user " + fullName);  // 4
    }
}

现在,假设有一个将表单发布到 userController.updateUser 的按钮。

我的假设是UserController.updateUser() 将在同一事务中执行userService.saveUser(user);logService.logEvent("Saved user " + fullName);。因此,如果对logService.logEvent() 的调用因 SQL 异常而失败,则不会更新用户实体。另外,我的假设是对userService.getFullName(user) 的调用不会在任何事务中运行,如果我们在用户名以 X 开头时过早退出该方法,则不会创建任何事务。但显然,这些只是猜测。

有人能解释一下Java EE 容器会做什么来支持UserController.updateUser() 方法与事务以及实际触发事务的原因是什么?此外,您可以指出我的任何进一步阅读,将不胜感激。我在网上看到了一些材料,但我仍然在这里遗漏了一些东西,并且在工作中也没有得到任何帮助。所以我当然不是唯一在这方面存在差距的人。

【问题讨论】:

    标签: jakarta-ee transactions entitymanager


    【解决方案1】:

    必须明确控制 Java EE 中的事务,或者使用 JNDI 中的 UserTransaction 或使用 EJB 上的部署描述符/注释。对于 CDI 组件,此处为 UserController,默认情况下不启动事务。 (EDIT如果未指定,则默认启用 EJB 方法的事务。)

    首先,你的假设:

    UserController.updateUser() 将执行 userService.saveUser(user);logService.logEvent("Saved user " + fullName);在同一笔交易中

    错了!我相信每次em.persist()/em.merge() 调用都会创建并提交一个新事务。

    为了将调用saveUser()logEvent() 包装在同一个事务中,您可以手动使用UserTransaction

    public void updateUser(user) {
        InitialContext ic = new InitialContext();
        UserTransaction utx = (UserTransaction) ic.lookup("java:comp/UserTransaction");
        utx.begin();
        ...
        utx.commit(); // or rollback(), wrap them in try...finally
    }
    

    或者更友好的:

    @Resource UserTransaction utx;
    ...
    public void updateUser(user) {
        utx.begin();
        ...
        utx.commit(); // or rollback(), wrap them in try...finally
    }
    

    或者甚至更好的 @Transactional 注释,无论是在 Java EE 7 中,还是在 Java EE 6 中使用 DeltaSpike JPA extension(或任何其他类似的拦截器)。

    @Transactional
    public void updateUser(user) {
        ...
    }
    

    您可以使用javax.ejb.TransactionAttribute 注释指定EJB 方法是事务性的。在这种情况下,仍然会有 2 笔交易。或者,您可以使用@TransactionAttribute 注释的方法将“业务”逻辑从 Web 层移动到 EJB 层,并实现在单个事务中运行 DB 方法。

    如需进一步阅读,请查看 EJB 3 规范中的“支持事务”一章。

    【讨论】:

    • 您必须使用 XML DD '激活' EJB 事务的评论是错误的。默认情况下,DD 是可选,并且 EJB 事务支持在容器管理环境中处于活动状态,并且在调用 EJB 方法时启动新的 TX。如果该方法调用其他 EJB 上或自身上的其他方法(通过 SessionContext 包装器或自注入实例),则还有 TX 传播规则。 @TransactionAttribute 注解在 CMT 中的使用是可选的,@TransactionAttribute(TransactionAttribute.REQUIRED) 默认是假定的。
    • 在我上一条评论中,@TransactionAttribute(TransactionAttribute.REQUIRED) 应该是 @TransactionAttribute(TransactionAttributeType.REQUIRED)
    • 您对每次调用 .persist() 或 .merge 都会创建一个新 TX 的评论是错误。您可以在 TX 中拥有任意数量的持久化或合并。默认情况下,在 CMT 中,TX 边界是 EJB 方法,它可以拥有任意数量的持久化或合并。此外,TX可以传播到其他 EJB 方法,默认情况下它是相同的 TX,但是,可以通过在调用的 EJB 类/方法上使用 @TransactionAttribute 注释来控制此 TX 传播。
    • (1) 我从来没有说过 DD 是强制性的 (2) 你是对的,事务 REQUIRED 是默认的,已更正 (3) 我从来没有说过你不能在一个交易;我说过在这种情况下和它的使用方式,每个操作都会有一个事务。这是基于我的错误假设,即默认情况下在 EJB 方法中不启动任何事务。尽管如此,在 OP 的代码中,persist()merge() 将在在它们自己的事务中被调用,所以其余的论点都是正确的。
    • 在我的评论针对您的原始回复中说,“...如果 XML DD 中没有为 EJB 指定任何内容,则也不会为它们启动任何事务。”我想我误解了你在那里试图表达的意思,因为这就是它听起来的样子。
    【解决方案2】:

    您需要将@Inject 注释更改为@EJB,然后在默认情况下使用CMT(容器管理事务)CDI bean 的每个调用都将在其自己的TX 范围内。如果您不希望其中一个方法调用调用 TX,则将 @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) 添加到方法中。

    从您的业务逻辑来看,您可以通过将 @Named 更改为 @Stateless、将 @Inject 更改为 @EJB 并在 getFullName(..) 上添加 @TransactionalAttribute(TransactionAttributeType.NOT_SUPPORTED) 来真正简化事情,如果您不这样做希望该方法在 TX 中运行。通过这些更改,您将获得所需的 TX 行为。

    如果您希望 UserController 成为 JSF 托管 bean,那么连同我建议的其他模块,我会将 @Named 更改为 @ManagedBean 并在 @ManagedBean 下添加 @Stateless 而不是将 @Named 更改为 @987654334 @。

    【讨论】:

    • NBW,感谢您的贡献,但我必须说我没有关注这篇文章。我最近读到的所有内容都建议放弃使用@EJB@ManagedBean,分别使用@Inject@Named。通过告诉我我应该使用它们,你把我送到了一个让我更加困惑的地方。我将进一步阅读 @EJB@Inject 之间的差异,但我相信发布这些建议是有原因的。
    • @jacekn 与该领域的大多数解决方案一样,没有正确或错误的答案,但有几个微妙的答案。如果您正在编写 EE6,那么您肯定会在 EE7 中使用 @Managed@EJB,您可以使用 @Inject@Named,或者您可以使用 EE6 方式。 EE7 不再使用 @Managed,因为它被视为无关紧要,这与 EE6 对启用 CDI 容器的空 beans.xml 的要求在 EE7 中被删除的方式非常相似。
    • @jacekn 主观上,我更喜欢将@EJB 用于我的 TX 管理逻辑,但是,由于在 EE5 之前的 EJB API 的缺点,他们名声不好。最近将 TX mgmnt 添加到 CDI 中的努力在某种程度上是为了将 EE 作为一个不需要“邪恶”EJB 词的堆栈进行营销。从 EE7 开始,CDI TX 支持不像 EJB 容器那样功能丰富和强大,恕我直言,EJB API 也同样容易使用。 David Levins(TomEE、OpenEJB 等)在这里有一个很好的比较 stackoverflow.com/questions/13487987/…
    【解决方案3】:

    在您的情况下,将启动 3 个独立事务。每个由您的 @Stateless bean 方法之一。这是因为会话 EJB 默认具有事务类型为TransactionAttribute.REQUIRED 的事务方法。这意味着如果一个事务尚未运行,新的事务将在方法调用之前创建。

    要在一个事务中运行所有会话 EJB 方法,您必须将它们包装在一个事务中。在您的情况下,您可以通过使用 @Transactional 注释 updateUser(...) 方法来做到这一点

    【讨论】:

    • 饺子,所以你告诉我容器默认情况下不为@Named 提供事务支持,就像它为@Stateless 提供的那样。您还告诉我userService.saveUser(user); 将开始并提交自己的事务,而logService.logEvent() 将无法访问它,因此必须开始自己的事务。请确认。
    • 没错,容器默认不提供@Named的事务支持。只有会话 EJB 有这种支持。是的,你是对的,@Stateless 中的每个方法都会启动自己的事务,因为这就是会话 EJB 默认的工作方式:)
    • @jacekn 这里有两个“容器”。一个是 EJB 容器,它将管理一个带有 @Stateless 以及其他注释的 bean。另一个容器是 CDI,它将管理(在其他 bean 中)带有 @Named 注释的容器。 CDI bean 默认情况下是事务性的,EJB bean 默认情况下是。此时,EJB 容器在事务管理空间中提供了比 CDI 更多的功能。
    • 嗨@flyingDumpling,所以@Transactional 也存在于JEE 中并且不仅有一个Spring 注释?
    【解决方案4】:

    对于仍然遇到此问题的任何人,接受的答案( 饺子)是错误的。

    实际发生的事情是这样的。容器默认具有 TransactionAttributeType = REQUIRED。这意味着如果您不注释任何 bean,它们将始终是必需的。

    现在发生的事情是这样的:

    你调用方法 UserController.updateUser() 并且当你这样做时,将创建一个事务(默认情况下,如果没有注释指示,容器会在每次执行方法时创建一个事务,并在执行结束)。

    当你调用 userService.getFullName(user) 时,由于这个方法是必需的,所以会发生的是,当你第一次调用 UserController.updateUser() 时最初启动的同一个事务将在这里再次使用。 然后容器返回到他的第一个 bean 并调用另一个方法 userService.saveUser(user),再次因为事务类型是 REQUIRED,所以将使用相同的事务。当它返回并调用第三种方法时,logService.logEvent("Saved user" + fullName) 使用相同的方法。

    在这种情况下,如果您想确保每个操作都在单独的事务中运行以避免在其中一个失败时回滚所有操作,您可以在与数据库交互的每个方法中使用 REQUIRES_NEW。这样,您可以确保每次运行方法时都会创建一个新事务,并且如果其中一个失败并且您希望继续执行其他事务,则不会造成任何伤害。

    更多信息可以在这里找到:https://docs.oracle.com/javaee/5/tutorial/doc/bncij.html

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-05-01
      • 2013-12-19
      • 1970-01-01
      • 2017-04-22
      • 2017-01-30
      • 1970-01-01
      • 2013-06-15
      相关资源
      最近更新 更多