grailsgrails-ormgrails-domain-class

Grails 4.1.4 integration test not able to update Domain class via Data service, unless using withNewTransaction


I have created a public Github repo replicating and illustrating the issue:

https://github.com/SparkersData/grails414-dataservice-int-test

Introduction

The integration test aims to update an existing Book object by calling a transactional service, which in turn calls a GORM Data service to update the Book.

BookServiceIntSpec -> [ BookService -> BookDataService ]

The execution of the portion between square brackets should be guarded by a Transaction introduced by the @Transactional annotation of the BookService

I have created 3 different Spec methods that demonstrate the odd behavior.

The preamble of the integration tests is a follows:

import acme.domain.Book
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
import spock.lang.Subject

@Integration
@Rollback
@Subject(BookService)
class BookServiceIntSpec extends Specification {

    BookService bookService

    Book book

    void setup() {
        book = new Book(title: "The Hobbit")
        book.save(flush: true, failOnError: true)
    }

    void cleanup() {
        Book.where {}.deleteAll()
    }
}

Test cases

It is able to update a Book without creating a new transaction, by using GORM in the service

Production code in BookService:

    Book updateWithGORM(Book toUpdate) {
        toUpdate.save(flush: true, failOnError: true)
    }

This is the Integration test:

    def "It is able to update a Book without creating a new transaction, by using GORM in the service."() {
        when:
        book.title = updatedTitle
        bookService.updateWithGORM(book)

        then:
        Book.findByTitle(updatedTitle) != null

        where:
        updatedTitle = "Bilbo The Hobbit"
    }

It is NOT able to update a Book without creating a new transaction, if using a Data Service

Production code in BookService

    Book updateWithDataService(Book toUpdate) {
        bookDataService.save(toUpdate)
    }

This is the Integration test:

    def "It is NOT able to update a Book without creating a new transaction, if using a Data Service."() {
        when:
        book.title = updatedTitle
        bookService.updateWithDataService(book)

        then:
        Book.findByTitle(updatedTitle) == null //<1>

        where:
        updatedTitle = "Bilbo The Hobbit"
    }

<1> The updated book cannot be found after requesting an update

It is able to update a Book by creating a new transaction, if using a Data Service

    Book updateWithDataService(Book toUpdate) {
        bookDataService.save(toUpdate)
    }

This is the Integration test:

    def "It is able to update a Book by creating a new transaction, if using a Data Service."() {
        when:
        book.title = updatedTitle
        Book.withNewTransaction { //<1>
            bookService.updateWithDataService(book)
        }

        then:
        Book.findByTitle(updatedTitle) != null //<2>

        where:
        updatedTitle = "Bilbo The Hobbit"
    }

<1> By usign withNewTransaction, the update succeeds
<2> The updated book CAN be found after requesting an update

Conclusion

Either this is a bug in the integration test runtime of Grails 4.1.4 or the documentation is not clear on how to use transactional services in integration tests. My guesses for the explanation of the behavior:

Has anyone else stumbled on this behavior or has an explanation of this? Wrapping every call with withNewTransaction seems like a nightmare.

Updates

Update 1: Explicitly flushing the session in the service method

By modifying the service method as follows:

    Book updateWithDataService(Book toUpdate) {
        def res = bookDataService.save(toUpdate)
        sessionFactory.currentSession.flush()
        res
    }

The tests behave properly, without having to mess with creating new transactions, and potentially disturbing the Hibernate session.

I still don't understand why this is not the default behavior!

Update 2: Is this a caching issue?

@mike-w Suggests that I may be hitting some kind of cache when using findByTitle in my test. I think, however, that it is, on the contrary, when he uses findById() that we are hitting a cache in my test.

This is substantiated by the fact that, when I activate hibernate tracing in logback.groovy:

logger("org.hibernate.type.descriptor.sql.BasicBinder", TRACE, ['STDOUT'], false)
logger("org.hibernate.SQL", DEBUG, ['STDOUT'], false)

Then I can see the following, if I keep the session unflushed:

2024-04-21 01:15:36.620 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as y0_ from book this_ where this_.title=? limit ?
2024-04-21 01:15:36.621 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [The Hobbit]
2024-04-21 01:15:36.621 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into book (id, version, title) values (null, ?, ?)
2024-04-21 01:15:36.622 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [0]
2024-04-21 01:15:36.622 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [The Hobbit]
2024-04-21 01:15:36.625 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as y0_ from book this_ where this_.title=? limit ?
2024-04-21 01:15:36.626 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Bilbo The Hobbit]
2024-04-21 01:15:36.627 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.title as title3_0_0_ from book this_ where this_.title=? limit ?
2024-04-21 01:15:36.627 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Bilbo The Hobbit]
2024-04-21 01:15:36.638 DEBUG --- [    Test worker] org.hibernate.SQL                        : delete from book

i.e. no update statement whatsoever.

If, however, i add sessionFactory.currentSession.flush() then I can see the following:

2024-04-21 01:21:29.612 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as y0_ from book this_ where this_.title=? limit ?
2024-04-21 01:21:29.613 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [The Hobbit]
2024-04-21 01:21:29.614 DEBUG --- [    Test worker] org.hibernate.SQL                        : insert into book (id, version, title) values (null, ?, ?)
2024-04-21 01:21:29.614 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [0]
2024-04-21 01:21:29.614 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [The Hobbit]
2024-04-21 01:21:29.618 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as y0_ from book this_ where this_.title=? limit ?
2024-04-21 01:21:29.618 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Bilbo The Hobbit]
2024-04-21 01:21:29.621 DEBUG --- [    Test worker] org.hibernate.SQL                        : update book set version=?, title=? where id=? and version=?
2024-04-21 01:21:29.621 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2024-04-21 01:21:29.621 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Bilbo The Hobbit]
2024-04-21 01:21:29.622 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [2]
2024-04-21 01:21:29.622 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [0]
2024-04-21 01:21:29.625 DEBUG --- [    Test worker] org.hibernate.SQL                        : select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.title as title3_0_0_ from book this_ where this_.title=? limit ?
2024-04-21 01:21:29.625 TRACE --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Bilbo The Hobbit]
2024-04-21 01:21:29.627 DEBUG --- [    Test worker] org.hibernate.SQL                        : delete from book

So I don't believe that in the first case the book has been updated at all, but in Mike's solution, we are hitting the cache when trying to get the Book by id (i.e. the updated book instance in the hibernate session of the integration test).


Solution

  • All right, I don't know what I was thinking.

    Sometimes leaving a problem a couple of days alone is all you need.

    Now it seems totally obvious to me that my expectation of the Session being flushed after the transactional service method exits was mistaken.

    It is an integration test, marked with @Rollback. This means that the test will run in a transaction that will be rolled back after the test concludes.

    The @Transactional service will not create a new transaction, it will join the existing transaction. The transaction by no means is concluded after the transactional service method returns. The transaction is still running inside my integration test.

    If I want to measure transactional effects I need to use withNewTransaction and make sure that the service joins a different transaction from my test.

    If I want to see the side effects of my service method, I need to flush the session. I can do that by injecting the SessionFactory in the integration test, and calling sessionFactory.currentSession.flush().

    So no bug at all. Unless you want to count me being confused.