(Quick Reference)

8.1 Declarative Transactions - Reference Documentation

Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith

Version: null

8.1 Declarative Transactions

h3. Default Declarative Transactions

聲明式事務

Services are typically involved with coordinating logic between domain classes, and hence often involved with persistence that spans large operations. Given the nature of services, they frequently require transactional behaviour. You can use programmatic transactions with the withTransaction method, however this is repetitive and doesn't fully leverage the power of Spring's underlying transaction abstraction.

Services通常會包含這樣的邏輯--需要多個domain類之間相互配合。因此它常常會出現這樣的情況:涉及到的持久化包括大量的數據庫操作。這些問題使得service中經常都需要對方法進行事務管理。當然你可以用withTransaction 方法來管理事務,但是這樣很繁瑣,也不能充分利用Spring的強大的事務抽象能力。

Grails中可以對service進行事務劃分,它聲明service中所有方法都是事務型的。缺省所有的service都進行了事務劃分。要禁用這個配置,只需要設置transactional 屬性為false:

Services enable transaction demarcation, which is a declarative way of defining which methods are to be made transactional. All services are transactional by default. To disable this set the transactional property to false:

class CountryService {
    static transactional = false
}

你也可以設置這個屬性為true,以防止將來這個默認值改變後對你的應用造成影響,或者是明確聲明service是事務型的。

You may also set this property to true to make it clear that the service is intentionally transactional.

警告: 依賴注入是使聲明式事務工作的唯一途徑。如果你自己用new操作符,比如new BookService(),將不能得到一個事務型的service.

Warning: dependency injection is the only way that declarative transactions work. You will not get a transactional service if you use the new operator such as new BookService()

這樣的結果是所有的方法被包裝在一個事務中,在方法中有拋出Runtime 異常Error時,將會自動回滾。事務傳播級別默認是PROPAGATION_REQUIRED.

Checked異常不會回滾事務. Groovy認為checked和unchecked異常非常相似,但Spring不知道這個道理並且使用默認值. 因此必須有了解checked和unchecked異常之間的差異。

The result is that all methods are wrapped in a transaction and automatic rollback occurs if a method throws a runtime exception (i.e. one that extends RuntimeException) or an Error. The propagation level of the transaction is by default set to PROPAGATION_REQUIRED.

Checked exceptions do not roll back transactions. Even though Groovy blurs the distinction between checked and unchecked exceptions, Spring isn't aware of this and its default behaviour is used, so it's important to understand the distinction between checked and unchecked exceptions.

Custom Transaction Configuration

Grails also fully supports Spring's Transactional annotation for cases where you need more fine-grained control over transactions at a per-method level or need specify an alternative propagation level.

Annotating a service method with Transactional disables the default Grails transactional behavior for that service (in the same way that adding transactional=false does) so if you use any annotations you must annotate all methods that require transactions.

In this example listBooks uses a read-only transaction, updateBook uses a default read-write transaction, and deleteBook is not transactional (probably not a good idea given its name).

自定事務配置

當你需要更細粒度的交易控制或需要指定另類傳播級別的時候,Grails也支持Spring的 Transactional註釋。

使用 Transactional註釋會停用Grails對該Service的默認行為。所以如果你使用任何註釋,你必須註解的所有方法

在這個例子中listBooks只使用只讀的事務,updateBook使用一個讀寫事務,deleteBook不是事務性的(看它的名稱這可能不是一個好主意)。

import org.springframework.transaction.annotation.Transactional

class BookService {

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

@Transactional def updateBook() { // … }

def deleteBook() { // … } }

您也可以註解全程Service的交易行為然後重寫每個方法的交易行為。這項Service是相當於一個沒有註釋的Service(因為默認值總是 Transactional= TRUE):

You can also annotate the class to define the default transaction behavior for the whole service, and then override that default per-method. For example, this service is equivalent to one that has no annotations (since the default is implicitly transactional=true):

import org.springframework.transaction.annotation.Transactional

@Transactional class BookService {

def listBooks() { Book.list() }

def updateBook() { // … }

def deleteBook() { // … } }

為這個例子中類級別的註釋保證所有方法為讀寫事務,但 listBooks方法重寫為只讀的交易:

This version defaults to all methods being read-write transactional (due to the class-level annotation), but the listBooks method overrides this to use a read-only transaction:

import org.springframework.transaction.annotation.Transactional

@Transactional class BookService {

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

def updateBook() { // … }

def deleteBook() { // … } }

雖然在這個例子中 updateBook deleteBook沒有註明註釋,它們繼承了類級別的註釋配置。

如需詳細資訊,請參閱Spring的用戶指南Using @Transactional.

Grails和Spring之間不同的特點是Grails使用 Transactional時不需要任何先前的配置。

Although updateBook and deleteBook aren't annotated in this example, they inherit the configuration from the class-level annotation.

For more information refer to the section of the Spring user guide on Using @Transactional.

Unlike Spring you do not need any prior configuration to use Transactional; just specify the annotation as needed and Grails will detect them up automatically.

8.1.1 Transactions Rollback and the Session

Understanding Transactions and the Hibernate Session

When using transactions there are important considerations you must take into account with regards to how the underlying persistence session is handled by Hibernate. When a transaction is rolled back the Hibernate session used by GORM is cleared. This means any objects within the session become detached and accessing uninitialized lazy-loaded collections will lead to LazyInitializationExceptions.

To understand why it is important that the Hibernate session is cleared. Consider the following example:

class Author {
    String name
    Integer age

static hasMany = [books: Book] }

If you were to save two authors using consecutive transactions as follows:

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

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

Only the second author would be saved since the first transaction rolls back the author save() by clearing the Hibernate session. If the Hibernate session were not cleared then both author instances would be persisted and it would lead to very unexpected results.

It can, however, be frustrating to get LazyInitializationExceptions due to the session being cleared.

For example, consider the following example:

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}" } } }

In the above example the transaction will be rolled back if the Author's age exceeds the maximum value defined in the isTooOld() method by throwing an AuthorException. The AuthorException references the author but when the books association is accessed a LazyInitializationException will be thrown because the underlying Hibernate session has been cleared.

To solve this problem you have a number of options. One is to ensure you query eagerly to get the data you will need:

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

In this example the books association will be queried when retrieving the Author.

This is the optimal solution as it requires fewer queries then the following suggested solutions.

Another solution is to redirect the request after a transaction rollback:

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 } } }

In this case a new request will deal with retrieving the Author again. And, finally a third solution is to retrieve the data for the Author again to make sure the session remains in the correct state:

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}" } } }

Validation Errors and Rollback

A common use case is to rollback a transaction if there are validation errors. For example consider this service:

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) } } }

To re-render the same view that a transaction was rolled back in you can re-associate the errors with a refreshed instance before rendering:

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] } } }