13.1.2 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 a LazyInitializationException.

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

  1. class Author {
  2. String name
  3. Integer age
  4. static hasMany = [books: Book]
  5. }

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

  1. Author.withTransaction { status ->
  2. new Author(name: "Stephen King", age: 40).save()
  3. status.setRollbackOnly()
  4. }
  5. Author.withTransaction { status ->
  6. new Author(name: "Stephen King", age: 40).save()
  7. }

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 a LazyInitializationException due to the session being cleared.

For example, consider the following example:

  1. class AuthorService {
  2. void updateAge(id, int age) {
  3. def author = Author.get(id)
  4. author.age = age
  5. if (author.isTooOld()) {
  6. throw new AuthorException("too old", author)
  7. }
  8. }
  9. }
  1. class AuthorController {
  2. def authorService
  3. def updateAge() {
  4. try {
  5. authorService.updateAge(params.id, params.int("age"))
  6. }
  7. catch(e) {
  8. render "Author books ${e.author.books}"
  9. }
  10. }
  11. }

In the above example the transaction will be rolled back if the age of the Author 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:

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

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

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

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

  1. class AuthorController {
  2. AuthorService authorService
  3. def updateAge() {
  4. try {
  5. authorService.updateAge(params.id, params.int("age"))
  6. }
  7. catch(e) {
  8. flash.message = "Can't update age"
  9. redirect action:"show", id:params.id
  10. }
  11. }
  12. }

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:

  1. class AuthorController {
  2. def authorService
  3. def updateAge() {
  4. try {
  5. authorService.updateAge(params.id, params.int("age"))
  6. }
  7. catch(e) {
  8. def author = Author.read(params.id)
  9. render "Author books ${author.books}"
  10. }
  11. }
  12. }

Validation Errors and Rollback

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

  1. import grails.validation.ValidationException
  2. class AuthorService {
  3. void updateAge(id, int age) {
  4. def author = Author.get(id)
  5. author.age = age
  6. if (!author.validate()) {
  7. throw new ValidationException("Author is not valid", author.errors)
  8. }
  9. }
  10. }

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:

  1. import grails.validation.ValidationException
  2. class AuthorController {
  3. def authorService
  4. def updateAge() {
  5. try {
  6. authorService.updateAge(params.id, params.int("age"))
  7. }
  8. catch (ValidationException e) {
  9. def author = Author.read(params.id)
  10. author.errors = e.errors
  11. render view: "edit", model: [author:author]
  12. }
  13. }
  14. }