3

Imagine you have the following controller in a Grails 2.5.5 application:

def index() {
        bookService.method()
        Book bako = Book.findById(4)
        System.out.println(bako.title);
}

And inside the bookService (with Grails default transaction management) you have the following method:

class BookService
    def method() {
        Book bako = Book.findById(4)
        System.out.println(bako.title);

        // MANUAL UPDATE OF DB HAPPENS HERE

        Book bako = Book.findById(4)
        System.out.println(bako.title);
    }
}

And that your db does have a Book with id 4 called "The Lord of The Rings".

If you then set breakpoints on all System.out.println() and, after the first findById has been executed, you manually edit the title of that object to "Harry Potter and the Goblet of Fire", I expected:

  • Both findById(4) in bookService.method() to read the same value, after all, they were performed in isolation in the same transaction, and if that transaction read it in that state, the second one should read it too.
  • The findById(4) performed in the controller to already see the new state of the object, after the manual update. After all, the first transaction has already commited, and this second find, even if done outside a service, should create a new transaction.

However, the output will always be the object in the same state it was at the beginning.

Update: The output is:

The Lord Of The Rings
The Lord Of The Rings
The Lord Of The Rings

If, in any case, you modifiy the controller, in order to:

def index() {
    bookService.method()
    Book.withNewTransaction {
        Book bako = Book.findById(4)
        System.out.println(bako.title);
    }

}

The result is still the same.

Update: The output is:

The Lord Of The Rings
The Lord Of The Rings
The Lord Of The Rings

Only if you modify it to:

def index() {
    bookService.method()
    Book.withNewSession {
        Book bako = Book.findById(4)
        System.out.println(bako.title);
    }

}

does the correct behavior ensue.

Update: The output is:

The Lord Of The Rings
The Lord Of The Rings
Harry Potter and the Goblet of Fire

Can someone explain me why:

  1. Just being outside the transaction that read an object in a certain state is not enough to read fresh data;
  2. Even forcing a new transaction is not enough to read the object with its most up to date state;
  3. Why is the new session what allows us to do so.
Miguel Pais
  • 73
  • 1
  • 5

1 Answers1

2

Firstly Book bako = Book.findById(4) findById should be used in rare cases refer to Book.get(1L) Book.load(1L) Book.read(1L)

You are firing up a query to look for id when you could have just run .get

Actual issue

After much talk, no matter how much a service is transactional. If you decide to update the DB using mysql manually. You will break hibernate cache. You can try disabling - first / second level cache. First being if you declared caching in your domain class mapping.

This is really unwise and will have application impact. The reality is a transaction service should be doing the updates for you. If you need to manually update the database. Stop the app update / start the app. It is really that simple

There is a reason why I have been trying to push you down the route of attempting this scenario using an example project.

Why ? Because it helps answer any speculation. I have taken a copy of your sample project and added some actual record updates to the demo. https://github.com/vahidhedayati/grails-transactions

I have also made a pull request on your version so you can merge it and test it locally.

Basically flush:true not required. .get(id) not required.

As you can see from the results below In service after .save() on method 1 the results was updated. In controller it returned the correct result using method() after service returned it.

-- transactions.Book : 1 1 added
| Server running. Browse to http://localhost:8080/transactions
2016-09-05 18:12:48,520 [http-bio-8080-exec-4] DEBUG hibernate.SQL  - select book0_.id as id1_0_0_, book0_.version as version2_0_0_, book0_.title as title3_0_0_ from book book0_ where book0_.id=?
--method1: before update: ------------------------------> TITLE_SET_BY_BOOTSTRAP
--method1 before get: ---------------------------------> New title from method1
method1  after get: ----------------------------------> New title from method1
2016-09-05 18:12:48,618 [http-bio-8080-exec-4] DEBUG hibernate.SQL  - update book set version=?, title=? where id=? and version=?
After service1 call 1 New title from method1
--method2  update: ------------------------------> New title from method1
2016-09-05 18:12:48,687 [http-bio-8080-exec-4] DEBUG hibernate.SQL  - update book set version=?, title=? where id=? and version=?
--method2 before get: --------------------------> New title from method2
method2 after get:  ----------------------------> New title from method2
After service call 2 New title from method2
--method3 before update: ---------------------------> New title from method2
2016-09-05 18:12:48,795 [http-bio-8080-exec-4] DEBUG hibernate.SQL  - update book set version=?, title=? where id=? and version=?
--method3 updated before get: -------------------------> New title from method3
--method3 after get: -----------------------------------> New title from method3
After service call 3 New title from method3

After reviewing the user issue ages back and having understood that they were manually updating DB record then expecting screen to show the same result.

In short if you have no cache enabled in the application then yes it should all work. If you have some form of Hibernate cache or ehcache enabled then it is likely you will be looking at some cache object. I had suggested restarting application to ensure you have latest. But if you simply:

wrap a

DomainClass.withNewTransaction { 
  //get the latest copy
  DomainClass clazz = DomainClass.get(recordId)
  println "-- ${clazz.value}"
}

This should ensure you are getting the very latest from the DB, it isn't going to be speed efficient but if you are expecting manual db updates you could always ensure the latest is above..

V H
  • 8,382
  • 2
  • 28
  • 48
  • But flush:true really goes against my mental model of how things should work in grails. If the code is inside transactional methods, flush:true should (almost) never be needed, unless inside a given transactional method that updated an object you need to read its most current status. But, if you update an object in one transactional method, and in another you read it, the first transaction should have been flushed and you should be able to read the most current status in the second. That's the core of my issue. Reference: http://stackoverflow.com/a/28302047/6788384 – Miguel Pais Sep 04 '16 at 10:49
  • Transactions issues have happened and contradicted all the time my mental model of how things should work, so I really created a sample test application **just** with the code I've displayed above. Here is the repo: https://github.com/miguelpais/grails-transactions – Miguel Pais Sep 05 '16 at 07:52
  • You shouldn't need @Transactional above the service method, they are transactional by default if you don't say anything. Yes, where I have the comment you're supposed to just change the object manually directly on the db, or update it from elsewhere. If you change the object in between the findById of the service method, my mental model tells me it's correct that the object returned is in the same state, since both findByid happened in the same transaction. However, the findById done later in the controller (after transaction commit) should return the new state, instead of the old one. – Miguel Pais Sep 05 '16 at 10:07
  • I hope that helps answer all your riddles – V H Sep 05 '16 at 17:23
  • Thank you, you definitely sent a lot of useful information! However, that magical line was just simply me going on MySql workbench or any other equivalent application and updating the record myself before second get(1) in the bookService would execute. In my MysqlLog I would clearly see the first find, the update (manual) commiting and the second find inside the same initial transaction returning the same (old) data. In the controller, however, I could find no excuse as for why the third find would not return the most up to date information and instead seems to return a cached version. – Miguel Pais Sep 05 '16 at 18:51
  • `Stop the app update / start the app` you're so funny. Don't confuse people. There are tons of issues when you need a manual sql execution. – Artem Novikov Jul 06 '17 at 18:52
  • @ArtemNovikov To be honest I were meaning to retouch on this a while back and haven't had time so will update my answer thank you – V H Jul 06 '17 at 19:03