如何正确将持久化后的JPA实体传递给异步EJB方法?
我想在Java EE 6(JPA用Eclipselink 2.5.2)环境下,基于已经存储到数据库的数据启动一个独立事务的异步任务——这样做的好处是能提前给请求返回结果,而且异步处理失败也不会影响原请求的处理(毕竟不在同一个事务里)。这些异步处理可能需要给数据添加额外信息。
我原本设想的实现是用@Asynchronous注解创建异步任务,代码大概是这样:
@Stateless public class Bean1 { @EJB Bean2 bean2; @PersistenceContext private EntityManager entityManager; public MyResult doSomething(MyInput input) { MyEntity myEntity = new MyEntity(); // 根据input填充数据 entityManager.persist(myEntity); bean2.asyncActOnData(myEntity); // 可能做些其他操作然后返回结果 } } @Stateless public class Bean2 { @Asynchronous // 会触发新事务 public void asyncActOnData(MyInput input){ // 处理数据并把结果存到数据库 } }
这种写法看起来简洁,但会导致JPA实体在Bean2处理时变成脱管状态。目前我暂时不需要在持久化和异步调用后修改数据,但随着应用规模扩大,我觉得允许修改数据能避免错误,会更安全。
请问这种基于已持久化数据启动独立异步任务的正确方式是什么?
你遇到的实体脱管问题,核心原因是异步方法调用时,Bean1的原事务已经提交(容器管理事务会在方法执行完毕时自动提交),传递过去的MyEntity实例已经脱离了原持久化上下文,自然没法在Bean2里安全地修改或持久化。结合Java EE 6和Eclipselink的特性,给你几个靠谱的实现方案:
传递实体ID而非实体实例(最推荐)
这是最稳妥的做法:在Bean1里执行persist后,实体已经拥有了主键(Eclipselink对自增主键的实体,persist时会立即生成主键值),你只需要把这个主键ID传给Bean2的异步方法。在Bean2中,用EntityManager.find()重新加载实体,此时拿到的是处于当前异步事务持久化上下文的托管实体,不管是读取还是修改都完全没问题。
调整后的代码示例:@Stateless public class Bean1 { @EJB Bean2 bean2; @PersistenceContext private EntityManager entityManager; public MyResult doSomething(MyInput input) { MyEntity myEntity = new MyEntity(); // 根据input填充实体数据 entityManager.persist(myEntity); // 获取实体主键,这里假设是Long类型 Long entityId = myEntity.getId(); bean2.asyncActOnData(entityId); // 其他业务逻辑并返回结果 return new MyResult(); } } @Stateless public class Bean2 { @PersistenceContext private EntityManager entityManager; @Asynchronous public void asyncActOnData(Long entityId){ // 重新加载实体,此时处于Bean2的独立事务上下文 MyEntity myEntity = entityManager.find(MyEntity.class, entityId); // 安全修改实体属性 myEntity.setExtraInfo("异步处理添加的额外信息"); // 因为是托管状态,事务提交时会自动同步到数据库,无需额外调用persist/merge } }确认异步方法的独立事务特性
你之前用@Asynchronous的思路是对的——Java EE中EJB的@Asynchronous方法默认事务属性是REQUIRED,当调用方(Bean1)的事务已经结束时,容器会自动为异步方法创建新的独立事务,完全符合你“异步处理失败不影响原请求”的需求。如果担心Bean1的事务还没提交,异步方法就开始执行导致查不到数据,可以在persist后调用entityManager.flush()强制将数据刷入数据库,不过一般容器会在Bean1方法执行完毕后自动提交事务,这个步骤不是必须的。如果必须传递实体实例(不推荐)
要是你因为某些原因必须传递实体实例,也可以在Bean2的方法里用entityManager.merge(myEntity)把脱管实体合并到当前持久化上下文——这个方法会返回一个托管的实体实例,之后你只能对这个返回的实例进行修改,才能在事务提交时同步到数据库。不过这种方式不如传递ID靠谱,因为脱管实体可能已经和数据库中的最新数据不一致,而重新find能保证拿到的是最新状态。
另外,关于你担心的“允许修改数据更安全”的需求,传递ID重新加载的方式完全满足:拿到的托管实体可以自由修改,容器会在异步事务提交时自动将变更同步到数据库,全程不需要手动处理持久化操作,既安全又省心。
内容的提问来源于stack exchange,提问作者Thirler

