JPA与CMT – 为什么单纯捕捉持久化异常是不够的?

在EJB和JPA架构中使用CMT(容器控制事务)是很简单的。只要定义一些注解来标示事务边界(或使用默认),无需再手动使用开始、提交或回滚操作。在EJB的业务方法中唯一回滚事务的方法就是抛出非应用异常(或者使用rollback=true标注应用异常)。看起来很简单:如果在某些操作中有可能会抛出一个异常,但你不希望事务回滚只需要捕获异常就可以了。你能够在同一个仍然活动的事务中再次重试这个不稳定的操作。

对于用户组件抛出的应用异常来说是完全成立的。问题是从其他组件抛出的异常会怎么样?比如JPA的EntityManager抛出的一个PersistenceException?这就是撰写本文的原因。

我们想要实现什么

想象一下如下场景:你有一个名为E的实体。包括如下属性:

  • id–主键,
  • name– 可读的实体名称,
  • content– 一些存放字符串的随意字段 – 用于模拟“高级属性”例如在持久化/更新时计算的字段并且可能导致错误的。
  • code– 保存OK 或者ERROR字符串 – 定义高级属性是否成功保存。

希望持久化E。假设E的基础属性一直都能成功持久化。而高级属性,需要一些附加计算或操作可能会导致例如数据库抛出的违反约束。当这样的情况发生后,你仍然希望E能够持久化到数据库中(只是基础属性填充,code字段设置为ERROR)
换句话说你可能希望:

  • 持久化E的基本属性
  • 尝试更新高级属性
  • 若步骤2中抛出了PersistenceException – 捕获该异常并设置‘code’属性为ERROR并且清除所有高级属性(因为会引发异常)
  • 更新E

普通解决方案
在EJB代码中展示你可能会如何执行(假设为默认的事务属性(TransactionAttributes))

public void mergeEntity() {
 MyEntity entity =newMyEntity('entityName','OK','DEFAULT');

 em.persist(entity);

 // This will raise DB constraint violation
 entity.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中的异常会使得事务回滚,而是EntityManager 抛出的非应用程序异常会将事务标记为回滚。更不用说资源内部可能会标记事务为回滚。意味着实际上你的程序并不能真正控制事务的行为。同时,作为事务回滚的结果,我们的实体被转移至游离(detached )状态。因此,在方法的最后 em.merge(entity)这样的方法还是需要的。

有效方案
该如何应对事务自动回滚呢?因为我们使用CMT,所以我们唯一的方法就是定义另一个业务方法开始一个全新的事务,并且将所有可能导致异常的操作在那里执行。这样的话即使PersistenceException 被抛出(并且捕获)它只会标注当前新事务回滚,我们的主事务是不会有影响的。如下可以看到一些样例代码(为了简洁去除了日志代码):

 public void mergeEntity() {

 MyEntity entity =newMyEntity('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(finalMyEntity entity) throws UpdateException {
 entity.setContent('tooLongContentValue');

 em.merge(entity);

 try{
 em.flush();
 }catch(PersistenceException e) {
 thrownewUpdateException();
 }
 }

注意
更新异常(UpdateExceptionis )也是继承自Exception类的应用异常(所以默认是rollback=false)。用于表示更新动作失败。作为一个可选方式,你可以将 tryMergingEntity(-)方法的返回值由void转为boolean。用这个boolean结果来表示更新动作成功与否。

Self在我们的EJB中是对自身的引用。这在EJB容器代理中是一个必须步骤,使得被调用的方法能够使用@TransactionAttribute,作为一个可选方式,可以使用SessionContext#getBusinessObject(clazz).tryMergingEntity(entity).

em.merge(entity) 方法是很关键的。我们在tryMergingEntity(-)方法中开启一个新事务,所以实体不会存在于持久化上下文中。
方法中无需再有其他的更新或刷新操作了。CMT的常规特性确保了事务不会被回滚,这意味着实体的所有改动都会在事务提交时自动刷新。
我们再次强调一下底线:捕获异常并不意味着你当前的事务不会被标记为回滚。PersistenceException 并发应用程序异常,但无论是你否捕捉都会导致你的事务回滚。
JTA BMT解决方案
一直以来我们都讨论的是CMT。那JTA BMT呢?找到下面代码中展示如何使用BMT处理这个问题作为奖励吧:

public void mergeEntity() throws Exception {
 utx.begin();
 MyEntity entity =newMyEntity('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我们能够在一个方法中处理所有的问题。是因为我们能够控制事务何时开始与提交/回滚(查看上述代码中的utx.begin()/commit()/rollback())。然而结果还是一样的,在抛出PersistenceException 之后,我们的事务还是被标记为回滚,你可以使用UserTransaction#getStatus() 来查看并且与常量Status.STATUS_MARKED_ROLLBACK进行比较。

 

英文原文,编译:ImportNew - 陈晨

译文链接:http://www.importnew.com/3922.html

【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】

关于作者: 陈 晨

致力于互联网大型分布式领域,实践各种软件过程与自动化。爱生活,爱JAVA。新浪微博: 一酌散千忧

查看陈 晨的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部