Grails 2.4.4:如何在复杂的服务方法中可靠地回滚

时间:2015-01-30 18:14:46

标签: exception grails rollback

考虑以下服务(默认情况下为事务性)。玩家必须始终拥有一个帐户。没有至少一个相应帐户的玩家是错误状态。

class playerService {
   def createPlayer() {
      Player p new Player(name: "Stephen King")
      if (!p.save()) {
         return [code: -1, errors:p.errors]
      }
      Account a = new Account(type: "cash")
      if (!a.save()) {
         // rollback p !
         return [code: -2, errors:a.errors]
      }
      // commit only now!
      return [code: 0, player:p] 
   }
 }

我已经看过经验丰富的Grails开发人员的这种模式,当我告诉他们如果播放器帐户的创建因任何原因失败时,它不会回滚播放器,并且会使数据库处于无效状态,他们看起来在我身边,因为我很生气,因为grails处理回滚玩家因为服务交易正确吗?

那么,作为一个SQL人,我寻找一种在grails中调用回滚的方法。没有一个。根据各种帖子,只有两种方法可以强制grails在服务中回滚:

  1. 抛出未经检查的异常。你知道这是对的吗?
  2. 不使用服务方法或事务注释,请使用以下结构:
  3. DomainObject.withTransaction {status ->
         //stuff
         if (someError) {
            status.setRollbackOnly()
         }
    }
    

    1。抛出未经检查的异常


    1.1因此我们必须抛出运行时异常以进行回滚。这对我来说没问题(我喜欢例外),但这不会与我们拥有的grails开发人员凝聚在一起,他们将异常视为对Java的回归而且是不酷的。这也意味着我们必须改变应用当前使用其服务层的整个方式。

    1.2如果抛出异常,则会丢失p.errors - 您将丢失验证详细信息。

    1.3我们的新grails devs不知道unchecked和checked异常之间的区别,也不知道如何区分。这真的很危险。

    1.4。使用.save(failOnError:true) 我是使用它的忠实粉丝,但它并不适合所有地方。有时您需要在进一步检查之前检查原因,而不是抛出异常。它可以生成的异常是否始终检查,始终未选中,或者是否?即willOnError AWLAYS会回滚,无论是什么原因?没有人问我知道答案,这是令人不安的,他们使用盲目的信念来避免损坏/不一致的数据库。

    1.5如果控制器调用服务A,调用服务B,然后调用服务C,会发生什么情况。服务A必须捕获任何异常,并向控制器返回格式良好的返回值。如果Service C抛出一个由服务A捕获的异常,那么将回滚服务Bs事务吗?这对于了解能够构建工作应用程序至关重要。

    更新1: 完成一些测试后,似乎任何运行时异常,即使抛出并捕获到一些不相关的子调用,也会导致父进程中的所有内容回滚。但是,在父会话中知道这种回滚已经发生并不容易 - 你需要确保如果你发现任何异常,你要么重新抛出,要么将一些通知传回给调用者以表明它已经失败了一种方式,其他一切将被回滚。

    2。 withTransaction


    2.1这似乎是一个集市建筑。我如何调用它,以及我为“status”参数传递了什么?什么是“setRollbackOnly”完全。为什么它不仅仅被称为“回滚”。什么是“唯一”部分?当你的方法可能想要更新几个不同的域对象时,它与域对象绑定。

    2.2你应该把这段代码放在哪里?在使用DomainObject类?在源文件夹中(即不在服务或控制器中?)?直接在控制器? (我们不想在控制器中复制业务逻辑)

    第3。理想的情况。


    3.1一般情况下,如果服务方法中的任何内容因任何原因无法保存,或者由于任何原因(已选中或未选中)抛出任何异常,我们希望我们在服务方法中执行的所有操作都回滚。

    3.2理想情况下,我希望服务方法“总是回滚,除非我明确地调用commit”,这是最安全的策略,但我认为这是不可能的。

    问题是如何实现理想状态?

    调用save(failOnError:true)总是回滚所有内容,无论失败的原因是什么?这并不完美,因为调用者不容易知道哪个域对象保存导致了问题。

    或者人们定义了许多子类为runtimeException的异常类,然后在控制器中显式捕获它们以创建适当的响应?这是旧的Java方式,由于我们将不得不编写大量的锅炉板代码,我们的常规开发人员对这种方法表示不满。

    人们使用什么方法来实现这一目标?

2 个答案:

答案 0 :(得分:4)

我不会称自己为专家,这个问题已经超过一年了,但我可以回答其中的一些问题,如果只是为了未来的搜索者。我现在正在重构一些控制器以使用服务以利用交易。

  

我已经看过经验丰富的Grails开发人员的这种模式,当我告诉他们如果播放器帐户的创建因任何原因失败时,它不会回滚播放器,并且会使数据库处于无效状态,他们看起来在我身边,因为我很生气,因为grails处理回滚玩家因为服务交易正确吗?

我没有在文档中看到它明确指出从服务方法返回回滚事务,但我无法想象这将是一个非常理智的行为。不过,测试是一种证明自己的简单方法。

  

1.2如果抛出异常,则会丢失p.errors - 您将丢失验证详细信息。

由于你是抛出异常的人,你可以随之抛出错误。例如:

// in service
    if (!email.save()) {
        throw new ValidationException("Couldn't save email ${params.id}", email.errors)
    }

当您捕获异常时,重新加载实例(因为抛出异常会清除会话),将错误放回实例中,然后照常将其传递给视图:

// in controller
    } catch (ValidationException e) {
        def email = Email.read(id)
        email.errors = e.errors
        render view: "edit", model: [emailInstance: email]
    }

http://grails.github.io/grails-doc/2.4.4/guide/single.html#transactionsRollbackAndTheSession的页面下方标题"验证错误和回滚"中讨论了这一点。

  

1.4。使用.save(failOnError:true)我很喜欢使用它,但它并不适合所有地方。有时您需要在进一步检查之前检查原因,而不是抛出异常。它可以生成的异常是否始终检查,始终未选中,或者是否?即willOnError AWLAYS会回滚,无论是什么原因?没有人问我知道答案,这是令人不安的,他们使用盲目的信念来避免损坏/不一致的数据库。

failOnError will cause save() to throw a ValidationException,是的,如果你在某个交易中并且没有检查该异常,那么该交易将被回滚。

一般来说,它似乎不是 - " Grailsy"大概使用failOnError,可能是因为你列出的原因(例如,缺乏控制)。相反,您需要检查save()是否失败(if (!save()) ...),并根据该行动采取行动。

  
      
  1. withTransaction
  2.   

我不确定这一点,因为SpringSource真的鼓励为所有事情使用服务。我个人也不喜欢它。

如果您想使特定服务非事务性,然后使其成为事务性的一种方法,您只需使用@Transactional注释一个方法(除非您的开发人员也不喜欢注释,因为他们' " Java&#34 ;;))。

请注意!只要使用@Transactional标记单个方法,整个服务就会变为非事务性的。

  

3.1一般情况下,如果服务方法中的任何内容因任何原因无法保存,或者由于任何原因(已选中或未选中)抛出任何异常,我们希望我们在服务方法中执行的所有操作都回滚。

我觉得检查异常通常被认为不是" Groovy" (这也使他们不是Grails-y)。不确定原因。

但是,看起来您可以通过在rollbackFor option to @Transactional中列出已检查的例外来告诉您的服务回滚。

  

或者人们定义了许多子类为runtimeException的异常类,然后在控制器中显式捕获它们以创建适当的响应?这是旧的Java方式,由于我们将不得不编写大量的锅炉板代码,我们的常规开发人员对这种方法表示不满。

Groovy的好处是你可以写一次锅炉板,然后反复调用它。我经常看到并且正在使用的模式是这样的:

private void validate(Long id, Closure closure) {
    try {
        closure()
    } catch (ValidationException e) {
        def email = Email.read(id)
        email.errors = e.errors
        render view: "edit", model: [emailInstance: email]
    } catch (OtherException e) {
        def email = Email.read(id)
        flash.error = "${e.message}: ${e.reasons}"
        render view: "show", model: [emailInstance: email]
    } catch (Throwable t) {
        flash.error = "Unexpected error $t: ${t.message}"
        redirect action: "list"
    }
}

然后在每个控制器动作中调用它,如下所示:

def update(Long id, Long version) {
    withInstance(id, version) { Email emailInstance ->
        validate(emailInstance.id) {
            emailService.update(emailInstance, params)
            flash.message = "Email $id updated at ${new Date()}."
            redirect action: "show", id: emailInstance.id
        }
    }
}

withInstance是另一种类似的方法,可以检查存在和乐观锁定。)

这种方法有缺点。您在每个操作中都获得相同的重定向集;你可能想为每个控制器编写一套方法;将闭包传递给方法并期望该方法知道闭包将抛出的异常似乎有点愚蠢。但是,嘿,编程都是关于权衡,对吧?

无论如何,希望这至少是有趣的。

答案 1 :(得分:0)

如果您有以下服务: 在Grails 2应用中,推荐的方法是使用transactionStatus.setRollbackOnly()

import grails.transaction.Transactional

Class RoleService {

    @Transactional
    Role save(String authority) {
        Role roleInstance = new Role(authority: authority) 
        if ( !roleInstance.save() ) {
           // log errors here
           transactionStatus.setRollbackOnly()
        }
        roleInstance
    }

}

请参阅:https://github.com/grails/grails-core/issues/9212