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

Joh*_*tle 10 grails exception 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] 
   }
 }
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 抛出未经检查的异常.你知道这是对的吗?
  2. 不要使用服务方法或事务注释,请使用此构造:

.

DomainObject.withTransaction {status ->
     //stuff
     if (someError) {
        status.setRollbackOnly()
     }
}
Run Code Online (Sandbox Code Playgroud)

1.抛出未经检查的异常


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

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

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

1.4.使用.save(failOnError:true)我很喜欢使用它,但它并不适合所有地方.有时您需要在进一步检查之前检查原因,而不是抛出异常.它可以生成的异常是否始终检查,始终未选中,或者是否?即无论是什么原因导致failOnError 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方式,我们的常规开发人员呸呸这种方法,因为我们将不得不编写锅炉板代码的数量.

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

Cha*_*ood 4

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

我见过经验丰富的 grails 开发人员的这种模式,当我告诉他们,如果玩家帐户的创建因任何原因失败,它不会回滚玩家,并且会使数据库处于无效状态,他们看着我就像我很生气,因为 grails 会回滚播放器,因为服务是事务性的,对吗?

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

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

由于您是抛出异常的人,因此您可以将错误与异常一起抛出。例如:

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

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

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

这将在http://grails.github.io/grails-doc/2.4.4/guide/single.html#transactionsRollbackAndTheSession页面下方的“验证错误和回滚”标题下进行讨论。

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

failOnError会导致save()抛出 aValidationException,所以是的,如果您处于事务中并且没有检查该异常,则事务将被回滚。

failOnError一般来说,大量使用似乎不“Grailsy” ,可能是由于您列出的原因(例如,缺乏控制)。相反,您检查是否save()失败 ( if (!save()) ...),并据此采取行动。

  1. 与交易

我不确定这有什么意义,因为 SpringSource 确实鼓励在所有事情上使用服务。我个人也不喜欢它。

如果您想让某个特定的服务成为非事务性的,然后将其一种方法设为事务性的,您只需使用 注释该方法即可@Transactional(除非您的开发人员也不喜欢注释,因为它们太“Java”了;))。

笔记!一旦您用 标记单个方法@Transactional,整个服务将变成非事务性的。

3.1 一般情况是,如果服务方法中的任何内容由于任何原因无法保存,或者由于任何原因(检查或未检查)抛出任何异常,我们希望在服务方法中所做的每一件事都回滚。

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

但是,您似乎可以通过在选项中列出它们来告诉您的服务回滚已检查的rollbackFor异常@Transactional

或者人们是否定义了许多异常类,这些异常类是 runtimeException 的子类,然后在控制器中显式捕获每个异常类以创建适当的响应?这是旧的 Java 方法,而我们的 groovy 开发人员对这种方法很不屑,因为我们必须编写大量的样板代码。

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"
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

withInstance是另一种类似的方法,用于干燥检查存在性和乐观锁定。)

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

不管怎样,希望这至少是有趣的。