0

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.

Aniket Kulkarni
  • 12,825
  • 9
  • 67
  • 90
Sap
  • 5,197
  • 8
  • 59
  • 101
  • I have been in similar situations, what happens if you call .merge() on the studentQuizInstance before calling it on studentInstance? – marko Jan 22 '14 at 14:33
  • There are couple of neat debugging tricks for situations like this (think this helped me solve a similar issue back in the day) - http://stackoverflow.com/questions/536601/what-are-your-favorite-grails-debugging-tricks – marko Jan 22 '14 at 14:34

1 Answers1

1

The core problem here is that the relationship is bidirectional, and both sides are changed and versioned. When you call addToAttempts, the attempts collection generated by the hasMany 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 and withTransaction 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

Burt Beckwith
  • 75,342
  • 5
  • 143
  • 156
  • No, there will be no concurrency issues if you're just adding a new record to the join table. One FK will point to the shared attempt, and the other to the current student. Other concurrent inserts will use the ids of those students. – Burt Beckwith Jan 22 '14 at 16:46
  • You can see an example of explicitly mapping the join table as a domain class in the http://grails.org/plugin/spring-security-core plugin - it uses this approach to map the many-many between users and roles. If you add `compile ":spring-security-core:2.0-RC2"` in the `plugins` section of `BuildConfig.groovy` (also include `mavenRepo "http://repo.spring.io/milestone/"` in the `repositories` block) and run `grails compile`, then `grails s2-quickstart test User Role` you'll see the UserRole.groovy domain class. – Burt Beckwith Jan 22 '14 at 16:49
  • In my case it works perfectly fine until the system comes under high usage. – Sap Jan 22 '14 at 16:50
  • Exactly - concurrency issues are like that. Everything's cool on your dev box, and in prod when there's no load, because you're not seeing enough traffic to observe collisions. – Burt Beckwith Jan 22 '14 at 16:50