(快速参考)

12.1 声明式事务

版本 6.2.0

12.1 声明式事务

声明式事务

服务通常涉及协调领域类之间的逻辑,因此常常涉及跨越大型操作的持久性。鉴于服务的性质,它们经常需要事务行为。您可以将编程事务与withTransaction方法结合使用,但这很重复,并没有充分利用 Spring 底层事务抽象的强大功能。

服务启用事务划分,这是一种声明式方法,用于定义哪些方法要进行事务处理。要在服务上启用事务,请使用Transactional转换

import grails.gorm.transactions.*

@Transactional
class CountryService {

}

结果是所有方法都包含在事务中,如果方法抛出异常(包括 Checked 或 Runtime 异常)或 Error,则会自动回滚。事务的传播级别默认设置为 PROPAGATION_REQUIRED

版本 Grails 3.2.0 是第一个默认使用 GORM 6 的版本。在 GORM 6 之前,Checked 异常不会回滚事务。只有抛出 Runtime 异常(即扩展 RuntimeException 的异常)的方法才会回滚事务。
警告:依赖注入是声明式事务适用的唯一方式。如果您使用new操作符(如new BookService()),则不会获得事务服务

Transactional 注释与 transactional 属性

在 Grails 3.1 之前版本的 Grails 中,Grails 创建了 Spring 代理,并使用 transactional 属性启用和禁用代理创建。在使用 Grails 3.1 及以上版本创建的应用程序中,默认情况下,这些代理被禁用,转而使用 @Transactional 转换。

对于 Grails 3.1.x 和 3.2.x 版本,如果您希望重新启用此功能(不推荐),则必须将 grails.spring.transactionManagement 设置为 true 或删除 grails-app/conf/application.ymlgrails-app/conf/application.groovy 中的配置。

在 Grails 3.3.x 中,用于事务管理的 Spring 代理已完全删除,您必须使用 Grails 的 AST 转换。在 Grails 3.3.x 中,如果您希望继续使用 Spring 代理进行事务管理,则必须使用适当的 Spring 配置手动配置它们。

此外,在 Grails 3.1 之前,服务默认是事务性的,从 Grails 3.1 开始,它们只有在应用了 @Transactional 转换的情况下才是事务性的。

自定义事务配置

对于需要针对每种方法级别更细粒度控制事务或需要指定备用传播级别的情况,Grails 还提供了 @Transactional@NotTransactional 注解。例如,当使用 @Transactional 注释类时,可以使用 @NotTransactional 注解标记要跳过的特定方法。

使用 Transactional 对服务方法进行注释会禁用该服务中默认的 Grails 事务行为(与添加 transactional=false 的方式相同),因此,如果您使用任何注释,则必须对需要事务的所有方法进行注释。

在此示例中,listBooks 使用只读事务,updateBook 使用默认读写事务,deleteBook 不是事务性的(考虑到它的名称,这可能不是个好主意)。

import grails.gorm.transactions.Transactional

class BookService {

    @Transactional(readOnly = true)
    def listBooks() {
        Book.list()
    }

    @Transactional
    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

您还可以对类进行注释,以定义整个服务的默认事务行为,然后按每种方法覆盖该默认值

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    def listBooks() {
        Book.list()
    }

    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

该版本默认为所有方法为读写事务(由于类级别注释),但 listBooks 方法覆盖了这一点以使用只读事务

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    @Transactional(readOnly = true)
    def listBooks() {
        Book.list()
    }

    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

在此示例中,虽然未对 updateBookdeleteBook 进行注释,但它们继承自类级别注释的配置。

有关更多信息,请参阅 Spring 用户指南中的关于 使用 @Transactional 一节。

与 Spring 不同,您无需任何之前的配置即可使用 Transactional;只需根据需要指定注释,Grails 就会自动检测到它们。

事务状态

TransactionStatus 的一个实例在 Grails 事务服务方法中默认为可用。

示例

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    def deleteBook() {
        transactionStatus.setRollbackOnly()
    }
}

12.1.1 事务和多数据源

给定两个域类,例如

class Movie {
    String title
}
class Book {
    String title

    static mapping = {
        datasource 'books'
    }
}

您可以向 @Transactional@ReadOnly 注解提供所需的数据源。

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
class BookService {

    @ReadOnly('books')
    List<Book> findAll() {
        Book.where {}.findAll()
    }

    @Transactional('books')
    Book save(String title) {
        Book book = new Book(title: title)
        book.save()
        book
    }
}
@CompileStatic
class MovieService {

    @ReadOnly
    List<Movie> findAll() {
        Movie.where {}.findAll()
    }
}

12.1.2 事务回滚和会话

理解事务和 Hibernate 会话

在使用事务时,您必须考虑到一些重要因素,了解 Hibernate 如何处理基本持久会话。当事务回滚时,GORM 使用的 Hibernate 会话被清除。这意味着会话中的任何对象将被分离,并且访问未初始化的延迟加载集合将导致 LazyInitializationException

下面来看个例子,解释会话被清除的重要性

class Author {
    String name
    Integer age

    static hasMany = [books: Book]
}

如果您使用连续事务保存两位作者,如下

Author.withTransaction { status ->
    new Author(name: "Stephen King", age: 40).save()
    status.setRollbackOnly()
}

Author.withTransaction { status ->
    new Author(name: "Stephen King", age: 40).save()
}

由于第一个事务通过清除 Hibernate 会话回滚了作者 save(),因此只有第二位作者被保存。如果 Hibernate 会话未被清除,那么两个作者实例都将被保存,并且会导致非常意外的结果。

但是,由于会话被清除,容易出现 LazyInitializationException,这可能会带来困扰。

例如,考虑以下示例

class AuthorService {

    void updateAge(id, int age) {
        def author = Author.get(id)
        author.age = age
        if (author.isTooOld()) {
            throw new AuthorException("too old", author)
        }
    }
}
class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            render "Author books ${e.author.books}"
        }
    }
}

如果 Author 年龄超过 isTooOld() 方法中定义的最大值,则该事务将由于抛出 AuthorException 而回滚。AuthorException 会引用此作者,但当访问 books 关联时,将抛出 LazyInitializationException,因为底层 Hibernate 会话已清除。

为解决这一问题,您有多种选择。一种方法是确保您积极查询以获取所需数据

class AuthorService {
    ...
    void updateAge(id, int age) {
        def author = Author.findById(id, [fetch:[books:"eager"]])
        ...

在此示例中,获取 Author 时将查询 books 关联。

这是最优解决方案,因为它比以下建议的解决方案需要更少的查询。

另一种解决方案是在事务回滚后重新定向请求

class AuthorController {

    AuthorService authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            flash.message = "Can't update age"
            redirect action:"show", id:params.id
        }
    }
}

在这种情况下,一个新的请求将负责再次检索 Author。最后,第三种解决方案是再次检索 Author 的数据,以确保会话保持正确状态

class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            def author = Author.read(params.id)
            render "Author books ${author.books}"
        }
    }
}

验证错误和回滚

常见用例是在出现验证错误时回滚事务。例如,考虑此服务

import grails.validation.ValidationException

class AuthorService {

    void updateAge(id, int age) {
        def author = Author.get(id)
        author.age = age
        if (!author.validate()) {
            throw new ValidationException("Author is not valid", author.errors)
        }
    }
}

要在事务回滚的同一个视图中重新呈现,可以在呈现前重新关联刷新实例和错误

import grails.validation.ValidationException

class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch (ValidationException e) {
            def author = Author.read(params.id)
            author.errors = e.errors
            render view: "edit", model: [author:author]
        }
    }
}