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
Update
button at a window. - Click
Update
button 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.Thread
org.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.