1 Introduction - Reference Documentation
Authors: Yasuharu NAKANO
Version: 0.4
1 Introduction
Grails supports for optimistic locking by default with implicitversion property of domain class.
When an user updates domain properties via user interface on browser, version's value must be passed via hidden form and be checked properly at a controller recieved the form.In a scaffolded controller of Grails 2.2.4, the version of the domain instance is compared with the value which is used when displaying the edit page, as follows:def update(Long id, Long version) { def sampleInstance = Sample.get(id) if (!sampleInstance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'sample.label', default: 'Sample'), id]) redirect(action: "list") return } if (version != null) { if (sampleInstance.version > version) { sampleInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'sample.label', default: 'Sample')] as Object[], "Another user has updated this Sample while you were editing") render(view: "edit", model: [sampleInstance: sampleInstance]) return } } sampleInstance.properties = params if (!sampleInstance.save(flush: true)) { render(view: "edit", model: [sampleInstance: sampleInstance]) return } flash.message = message(code: 'default.updated.message', args: [message(code: 'sample.label', default: 'Sample'), sampleInstance.id]) redirect(action: "show", id: sampleInstance.id) }
save method might cause a conflict error at the layer of Hibernate.
To easy to reproduce the problem, append a sleep line into the code, like this:def update(Long id, Long version) { def sampleInstance = Sample.get(id) if (!sampleInstance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'sample.label', default: 'Sample'), id]) redirect(action: "list") return } if (version != null) { if (sampleInstance.version > version) { sampleInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'sample.label', default: 'Sample')] as Object[], "Another user has updated this Sample while you were editing") render(view: "edit", model: [sampleInstance: sampleInstance]) return } } sampleInstance.properties = params sleep 10000 // TEMP <---------------------------------------------- HERE!! if (!sampleInstance.save(flush: true)) { render(view: "edit", model: [sampleInstance: sampleInstance]) return } flash.message = message(code: 'default.updated.message', args: [message(code: 'sample.label', default: 'Sample'), sampleInstance.id]) redirect(action: "show", id: sampleInstance.id) }
- Run the application by run-app command
- Open the edit pages of the same domain instance on two independent browser windows
- Click
Updatebutton at a window. - Click
Updatebutton at another window within 10 seconds.
| Error 2013-03-26 14:17:40,852 [http-bio-8080-exec-1] ERROR errors.GrailsExceptionResolver - StaleObjectStateException occurred when processing request: [POST] /grails-sample/sample/index - parameters:
id: 1
_action_update: Update
value: XXXXX
version: 1
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [grails.sample.Sample#1]. Stacktrace follows:
Message: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [grails.sample.Sample#1]
Line | Method
->> 78 | update in grails.sample.SampleController
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| 195 | doFilter in grails.plugin.cache.web.filter.PageFragmentCachingFilter
| 63 | doFilter in grails.plugin.cache.web.filter.AbstractFilter
| 1145 | runWorker in java.util.concurrent.ThreadPoolExecutor
| 615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^ 722 | run in java.lang.Threadorg.springframework.dao.OptimisticLockingFailureException (wrapping net.sf.hibernate.StateObjectStateException) like this:def update(Long id, Long version) { def sampleInstance = Sample.get(id) if (!sampleInstance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'sample.label', default: 'Sample'), id]) redirect(action: "list") return } if (version != null) { if (sampleInstance.version > version) { sampleInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'sample.label', default: 'Sample')] as Object[], "Another user has updated this Sample while you were editing") render(view: "edit", model: [sampleInstance: sampleInstance]) return } } sampleInstance.properties = params sleep 10000 // TEMP for easily reproducingreproducing try { if (!sampleInstance.save(flush: true)) { render(view: "edit", model: [sampleInstance: sampleInstance]) return } flash.message = message(code: 'default.updated.message', args: [message(code: 'sample.label', default: 'Sample'), sampleInstance.id]) redirect(action: "show", id: sampleInstance.id) } catch (OptimisticLockingFailureException e) { // It isn't DRY!! sampleInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'sample.label', default: 'Sample')] as Object[], "Another user has updated this Sample while you were editing") render(view: "edit", model: [sampleInstance: sampleInstance]) } }
def update(Long id, Long version) { def sampleInstance = Sample.get(id) if (!sampleInstance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'sample.label', default: 'Sample'), id]) redirect(action: "list") return } sampleInstance.withOptimisticLock(version) { sampleInstance.properties = params sleep 10000 // TEMP for easily reproducing if (!sampleInstance.save(flush: true)) { render(view: "edit", model: [sampleInstance: sampleInstance]) return } flash.message = message(code: 'default.updated.message', args: [message(code: 'sample.label', default: 'Sample'), sampleInstance.id]) redirect(action: "show", id: sampleInstance.id) }.onConflict { domain -> render(view: "edit", model: [sampleInstance: sampleInstance]) } }
withOptimisticLock method compare the version and catch OptimisticLockingFailureException and set a field error with default message to domain instance on conflict.