Reputation: 1836
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)?
Upvotes: 0
Views: 1821
Reputation: 2659
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!
Upvotes: 3
Reputation: 6540
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.
Upvotes: 2