Checked exceptions rollback transactions since GORM 6.0.0

27 Jun 2017

Tags: Grails, GORM

Programming Groovy 2:

Groovy has less ceremony than Java. That’s crystal-clear in exception handling. Java forces us to handle checked exceptions. Groovy does not force us to handle exceptions that we don’t want to handle or that are inappropriate at the current level of code. Any exception we don’t handle is automatically passed on to a higher level.

Although the distinction between checked exceptions and runtime exceptions in Groovy is blurred, GORM versions before 6.0.0 did not roll back transactions for checked Exceptions. However, they rollbacked transactions for runtime exceptions.

Grails 3.2.0 shipped with GORM 6.0.0. One of the changes introduced by GORM 6.0.0 is that if a transactional method throws an Exception (both checked or runtime exception), the transaction will automatically be rolled back. Thus, same behavior for checked and runtime exceptions as you may expect in Groovy.

The previous behavior is better illustrated with an example:

Given a Checked Exception

package demo

import groovy.transform.CompileStatic

@CompileStatic
class CustomCheckedException extends Exception {
}

and a RuntimeException

package demo

import groovy.transform.CompileStatic

@CompileStatic
class CustomRuntimeException extends RuntimeException {
}

and the next service:

package demo

import grails.transaction.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Transactional
class BookService {

    void addNewBookChecked() {
        def book = new Book(name: 'The definitive guide to grails 3')
        book.save()
        throw new CustomCheckedException()
    }

    void addNewBookRuntime() {
        def book = new Book(name: 'The definitive guide to grails 3')
        book.save()
        throw new CustomRuntimeException()
    }

    void addNewBookRegular() {
        def book = new Book(name: 'The definitive guide to grails 2')
        book.save()
    }

    @Transactional(readOnly = true)
    int count() {
        Book.where { }.count() as int
    }
}

The next specification passes for versions of GORM 6.0.0 or beyond.

package demo

import spock.lang.Specification
import spock.lang.Subject
import grails.test.mixin.integration.Integration

@Integration
class BookServiceIntegrationSpec extends Specification {

    @Subject
    BookService bookService

    def "calling a service method which does not throw any exception results in adding a new book"() {
        when:
        bookService.addNewBookRegular()

        then:
        bookService.count() == old(bookService.count()) + 1
    }

    def "calling a service method which throws a runtime exception, rollback transaction and it does not add a new book"() {
        when:
        bookService.addNewBookRuntime()

        then:
        thrown CustomRuntimeException
        bookService.count() == old(bookService.count())
    }

	// Fails with GORM versions < 6.0.0
    def "calling a service method which throws a Checked exception, rollback transaction and it does not add a new book"() {
        when:
        bookService.addNewBookChecked()

        then:
        thrown CustomCheckedException
        bookService.count() == old(bookService.count())
    }
}

Published on 27 Jun 2017