I have the following code
studentInstance.addToAttempts(studentQuizInstance)
studentInstance.merge()
studentInstance.save(flush:true)
and it throws following exception at the last line of above code
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.easytha.Quiz#1]
I have seen a couple of threads discussing the same issue and according to them I have tried using studentInstance.withTransaction
also studentInstance.withTransaction
and I also changed the service's scope to request but nothing helped so far.
This definitely is a threading issue because this only happens when 20 to 30 users call this code simultaneously.
The core problem here is that the relationship is bidirectional, and both sides are changed and versioned. When you call
addToAttempts
, theattempts
collection generated by thehasMany
property is initialized to a new empty collection if it's null, then the instance is added to it, and the instance's Student field is set to the owning student to ensure that the in-memory state is the same as it will be later if you reload everything from the database. But when you have versioning (optimistic locking) enabled, since both sides changed, both get a version bump. So if you have overlap with a collection between two concurrent users, you get this error. And it's real - you run the risk of losing a previous update if you don't explicitly lock, or use optimistic locking.But this is all entirely artificial. This looks like a many-to-many, so all you want is to add a new row in the join table that points to the student and the attempt. Grails does this by configuring collections that Hibernate detects changes in, but this is really taking advantage of a side effect. It's also very expensive for large collections. I left out one part above about the call to
addToAttempts
; if there were already instances there, every one will be retrieved from the database, even though you need none of them. Grails loads all N previous elements (where N can be a very large number) and adds a new N+1st, all so Hibernate can detect that new element. All you want is to insert one row, and you end up with a significant amount of database traffic.The fix isn't to scatter in
merge
andwithTransaction
calls, or other random stuff you find here or elsewhere - it's to remove the concurrent access. Here you're lucky since it's entirely artificial. See this talk I did a while back that's sadly still just as relevant with current Grails as it was then - I describe approaches for removing collections and replacing them with much more sensible approaches: http://www.infoq.com/presentations/GORM-Performance