0

Using Grails 2.3.9 (Groovy (2.2.2), Mysql 5.5.37 (MySQLUTF8InnoDBDialect), JDK 1.7

I'm trying to implement and test the optimistic locking feature from Grails/Hibernate in the controller side.

After my intuition the following

def instance = Group.findByXXX(...)
instance.properties = params
// ...
instance.version = 5 // something smaller than the current
instance.save flush:true, failOnError: true

would throw an exception is thrown because of wrong version. However, the instance is saved in any case.

This question is probably the same as this one, just that I don't understand it. This is what I tried after reading the this question/answer:

def copyInstance = copy(instance)   // I instantiate a new item, copy all members
                                    // from instance to the new one
copyInstance = copyInstance.merge()
copyInstance.version = 5            // something smaller than the current
copyInstance.save flush:true, failOnError: true

This had the expected result (saving failed). But I still don't quite see through it: could someone explain what the difference is between the upper object "instance" and the lower "copyInstance"? And, is this the way the optimistic locking is achieved (it seems to me the extra copying might not be needed)?

Community
  • 1
  • 1
tokosh
  • 1,772
  • 3
  • 20
  • 37
  • Possible duplicate http://stackoverflow.com/questions/18861891/how-do-i-know-if-a-grails-model-was-changed-since-its-retrieval . – dmahapatro Jun 05 '14 at 04:30

2 Answers2

3

TL;DR: your first example fails to crash because you're interfering with the versioning feature in the wrong way...

Although the accepted answer is broadly correct, it does get a few points of order incorrect, and I thought it would be worth adding a few clarifications. I think it's quite important to understand understand what is happening under the hood with optimistic locking, hence my pedantry here.

From the accepted answer: "Your top example works because you only have the one instance, so it's state can be transparently monitored without the need for locking"

Firstly, "optimistic locking" is a misnomer; there's no locking going on anywhere in the process (it's just versioning - pessimistic locking does use locks - it's confusing) so the handwavy "can be transparently monitored" makes the underlying process sound a lot more complicated than it is.

What is really happening: when an update is issued, Hibernate includes the version number of the object from when it was first loaded in this transaction into the resulting update statement. If zero rows are updated, the OptimisticLockException is thrown.

So that answers why your first example runs without error...

def instance = Group.findByID(49)

results in:

select * from tbl_group where id=49

Let's say the version was 28. Hibernates keeps a copy of all of the properties of an object when it is loaded - it does this so it can do a dirty check when the transaction is committed - if none of the values have changed, it doesn't need to do an update.

So this line of code has no effect on the version number that hibernate will use in the update:

instance.version = 5 // something smaller than the current

When you eventually call save:

instance.save flush:true, failOnError: true

This will result in

update tbl_group set version=**29** where id=49 **and version=28**

(BTW The logic in Hibernate is if this update fails to update any rows, then another transaction must have modified that row and incremented its version number.)

So that's why you're seeing no exception, the version used was the version number originally read in by this transaction, not the version number that you artificially wrote into the object.

A better test of this functionality would be to pause the execution just before the save() is executed (eg using a debugger). Now go into the database and change the version column for that row (to a higher number). Now the save() will fail with the OptimisticLockException because Hibernate thinks that another transaction modified that row first.

[I'd like to answer why the second example DID throw an exception, but I'd need to know how you did the copy (did all members including the ID copy over?) and did it definitely fail with OptimisticLockException or something else? Whatever reason, it's a weird set of operations that aren't realistic or illustrative so although it might be a good intellectual exercise to figure out what's happening, I'll leave that to others!]

Edit to add: I've run a test and the second example does indeed throw an OptimisticLockException - BUT it's at the merge() BEFORE you even change the version number. So you're getting the right outcome for the wrong reason. I would investigate further but it's an old question and I doubt anyone cares - but my rough feeling is that by copying the object and trying to have two objects representing the same persistent entity, the entitymanager is getting confused and raising it as an optimistic lock exception. I'm sure there's a fuller explanation (it's something to do with the select that is issued as step 1 of the merge) and I might return to that another day. Basically, Hibernate will do odd things if you give it odd things to do!

Dick Chesterwood
  • 2,629
  • 2
  • 25
  • 33
  • Thanks for the explanation. Yes, it's too long ago and I have not touched grails recently. – tokosh Jan 18 '18 at 01:29
  • @tokosh you're very very lucky! Thankfully I revived this as part of a project to retire our very last Grails application. The day of deletion can't come soon enough ;-) – Dick Chesterwood Jan 19 '18 at 13:04
2

AFAIK locking in Hibernate really only comes into play when you have 2 concurrent versions of the same persisted object within the session context.

Your top example works because you only have the one instance, so it's state can be transparently monitored without the need for locking. Hibernate knows that there is no possibility of your object having two different persisted states because there is only one instance of it, so it doesn't bother to check the version. It knows that this object is newer than what is in the database so it just writes your changes.

Your second instance fails because you have 2 instances of the same object. When you try to save the 2nd instance with the lower version, it fails because the object has been locked by the database. Hibernate will use the version number to determine which of the objects is newer, and persist those changes to the database.

JamesENL
  • 6,400
  • 6
  • 39
  • 64
  • So, if I want to have this optimistic locking on a REST API, I need to do it as I did in the second attempt? – tokosh Jun 05 '14 at 04:09
  • 1
    Optimistic locking will happen if you have more than one user concurrently accessing your objects. So if both users request the same object, then Hibernate will transparently handle the locking mechanism – JamesENL Jun 05 '14 at 04:10
  • I think, what I want to do hasn't really anything to do with the optimistic locking of hibernate. It's just a versioning conflict with which I have to deal myself (either using approach 2 or adding a check before a save/delete). – tokosh Jun 05 '14 at 04:17
  • Just make sure you take into account JB Nizet's comments on that question you linked, and don't forget to accept my answer if you think its right. – JamesENL Jun 05 '14 at 04:22