使用CMT( 容器管理的事务 )进入EJB和JPA的世界非常舒适。 只需定义一些注释来划分事务边界即可(或使用默认值),仅此而已–无需摆弄手动开始,提交或回滚操作。 回滚事务的一种方法是从EJB的业务方法中引发非应用程序异常(或具有rollback = true的应用程序异常)。 看起来很简单:如果在某些操作过程中可能会引发异常,并且您不想回滚tx,那么您应该捕获该异常就可以了。 现在,您可以在同一仍处于活动状态的事务中再次重试该易失性操作。
现在,对于从用户组件抛出的应用程序异常,这一切都是正确的 。 问题是– 除了其他组件引发的异常之外,还有什么? 就像JPA的EntityManager
抛出PersistenceException
? 这就是故事的开始。
我们想要实现的目标
设想以下情形:您有一个名为E的实体。它包含:
- id –这是主键,
- 名称 -这是一些易于理解的实体名称,
- 内容 -包含字符串的任意字段-它模拟“高级属性”,例如,在持久性/合并期间进行计算会导致错误。
- 代码 –包含OK或ERROR字符串–定义高级属性是否成功持久保存,
您要持久化E。您假定E的基本属性将始终被成功持久化。 但是,高级属性需要一些额外的计算或操作,这可能会导致例如从数据库引发约束冲突。 如果发生这种情况,您仍然希望E保留在数据库中(但仅填充基本属性,并且将代码属性设置为“ ERROR”)。
换句话说,这就是您可能想到的:
- 坚持E的基本属性,
- 尝试使用脆弱的高级属性进行更新,
- 如果从步骤2抛出了
PersistenceException
捕获它,将'code'属性设置为“ ERROR”并清除所有高级属性(它们导致异常), - 更新E。
天真的解决方案
转到EJB的代码,这就是您可以尝试执行的方式(假设使用默认的TransactionAttributes):
public void mergeEntity() {MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');em.persist(entity);// This will raise DB constraint violationentity.setContent('tooLongContentValue');// We don't need em.merge(entity) - our entity is in managed mode.try {em.flush(); // Force the flushing to occur now, not during method commit.} catch (PersistenceException e) { // Clear the properties to be able to persist the entity.entity.setContent('');entity.setCode('ERROR');// We don't need em.merge(entity) - our entity is in managed mode.}
}
这个例子有什么问题?
捕获由EntityManager
抛出的PersistenceException
不会阻止事务回滚 。 并不是在EJB中不缓存异常会使tx标记为回滚。 这是EntityManager
抛出的非应用程序异常 ,将tx标记为回滚。 更不用说资源可能会在其内部将tx标记为回滚。 这实际上意味着您的应用程序实际上无法控制此类tx行为。 此外,由于事务回滚,我们的实体已移至分离状态。 因此,此方法末尾需要一些em.merge(entity)
。
工作方案
那么如何处理这种自动事务回滚? 因为我们使用的是CMT,所以唯一的方法是定义另一种业务方法,该方法将启动新的事务并在那里执行所有易碎的操作 。 这样,即使将抛出(并捕获) PersistenceException
,它也将仅标记要回滚的新事务。 我们的主要TX将保持不变。 在下面,您可以从此处看到一些代码示例(为简洁起见,删除了日志记录语句):
public void mergeEntity() {MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');em.persist(entity);try {self.tryMergingEntity(entity);} catch (UpdateException ex) {entity.setContent('');entity.setCode('ERROR');}
}@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void tryMergingEntity(final MyEntity entity) throws UpdateException {entity.setContent('tooLongContentValue');em.merge(entity);try {em.flush();} catch (PersistenceException e) {throw new UpdateException();}
}
注意:
-
UpdateException
是@ApplicationException
,它扩展了Exception(因此默认情况下为rollback=false
)。 用于通知更新操作失败。 或者,您可以更改tryMergingEntity(-)
方法签名以返回布尔值而不是void。 该布尔值可以描述更新是否成功。 -
self
是对我们自己的EJB的自我引用。 这是使用EJB容器代理的必需步骤,该代理使被调用方法的@TransactionAttribute起作用。 或者,您可以使用SessionContext#getBusinessObject(clazz).tryMergingEntity(entity)
。 -
em.merge(entity)
是至关重要的。 我们正在tryMergingEntity(-)
中开始新事务,因此该实体不在持久性上下文中。 - 此方法不需要任何其他合并或刷新。 tx尚未回滚,因此批准了CMT的常规功能,这意味着在tx提交期间将自动刷新对实体的所有更改。
让我们再次强调最重要的一点: 如果您捕获到异常,这并不意味着您的当前事务没有被标记为回滚。 PersistenceException
不是ApplicationException,即使您是否捕获它,也将使您的tx回滚。
JTA BMT解决方案
我们一直在谈论CMT。 JTA BMT呢? 好吧,作为奖励,请找到以下代码,该代码显示了如何使用BMT处理此问题(也可在此处访问):
public void mergeEntity() throws Exception {utx.begin();MyEntity entity = new MyEntity('entityName', 'OK', 'DEFAULT');em.persist(entity);utx.commit();utx.begin();entity.setContent('tooLongContentValue');em.merge(entity);try {em.flush();} catch (PersistenceException e) {utx.rollback();utx.begin();entity.setContent('');entity.setCode('ERROR');em.merge(entity);utx.commit();}
}
使用JTA BMT,我们可以用一种方法完成所有这一切。 这是因为我们控制着tx何时开始以及提交/回滚 (看看那些utx.begin()/ commit()/ rollback()。尽管如此,结果还是一样的–抛出PersistenceException
我们的tx被标记为回滚然后可以使用UserTransaction#getStatus()
进行检查,并将其与诸如Status.STATUS_MARKED_ROLLBACK之类的常量进行比较,并可以在我的GitHub帐户上检查整个代码。
参考: JPA和CMT –为什么捕获持久性异常不足? 从我们的JCG合作伙伴 Piotr Nowicki在Piotr Nowicki的首页博客中获得。
翻译自: https://www.javacodegeeks.com/2013/03/jpa-and-cmt-why-catching-persistence-exception-is-not-enough.html