(Quick Reference)

1 Introduction - Reference Documentation

Authors: Yasuharu NAKANO

Version: 0.4

1 Introduction

Grails supports for optimistic locking by default with implicit version 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) }

But this code isn't enough to care about conflict of users' operations. After comparing version, 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) }

Procedure to reproduce:

  1. Run the application by run-app command
  2. Open the edit pages of the same domain instance on two independent browser windows
  3. Click Update button at a window.
  4. Click Update button at another window within 10 seconds.

Then you will see an error page and there are the following error message at console:

| 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

Yes. You must handle 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]) } }

This works well but it's ugly because there are duplicated codes.

If you use this plugin, you can just simply write the code 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
    }

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.