(Quick Reference)

Domain Locking - 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.

2 Optimistic Locking

In order to handle optimistic locking, use withOptimisticLock method which is injected to domain class by the plugin.
  • withOptimisticLock method compares a version of modification base with current version.
    • withOptimisticLock's closure is invoked only when "version of modification base" < "current persistent version" is true.
  • When "version of modification base" < "current persistent version" is false or OptimisticLockingFailureException is catched,
    • it invokes onConflict closure if exists.
    • it sets a field error message to the domain instance.
  • When an exception except OptimisticLockingFailureException is thrown, it's not caught and just thrown up.

sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->

// Operations which might causes OptimisticLockingFailureException. // Here is invoked only when a version of modification base < current persistent version.

}.onConflict { Object domain, Throwable caused ->

// Operations to handle a failure of optimistic locking // e.g. to render edit page to re-input }

See also Optimistic and Pessimistic locking of Grails User Guide.

Type of modificationBaseVersion

modificationBaseVersion allows either Number (Long, Integer, etc.) or String. So you can pass a value of params in a controller.

Skip a version comparation

If a copmaration of version is unnecessary, you can omit the modificationBaseVersion argument as follows:

sampleDomain.withOptimisticLock { Object domain ->
    // Here is always invoked regardless of the versions.
}

In this case, the main closure will be always invoked.

onConflict is optional

If you have nothing to do when a conflict occurs, you can omit onConflict closure:

sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
    // …
}

In this case, when something fails, it just only set error message to domain class.

Closure arguments

The first argument of the closure equals to delegate object. So you can use each one as you want.

sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
    assert domain.is(sampleDomain)
}.onConflict { Object domain ->
    assert domain.is(sampleDomain)
}

All arguments of onConflict's closure are optional. So you can omit them. The followings are all valid.

sampleDomain.withOptimisticLock { /* … */ }.onConflict { Object domain, Throwable caused -> /* … */ }
sampleDomain.withOptimisticLock { /* … */ }.onConflict { Object domain -> /* … */ }
sampleDomain.withOptimisticLock { /* … */ }.onConflict { -> /* … */ }

Return value

If you want to use a return value of a closure, you can get it from returnValue property.

def result = sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
    return "OK"
}.onConflict { Object domain, Throwable caused ->
    return "NG"
}
assert result.returnValue == "OK"

In case of a conflict, a return value of a onConflict's closure is returned.

assert result.returnValue == "NG"

Flushing session

In case of using persistence methods (e.g. save method), you must be flush a session.

def result = sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
    domain.save(flush: true)
}.onConflict { Object domain, Throwable caused ->
    return "NG" // In case of error, you can catch and handle it here.
}

Otherwise, a conflict error occurs on flushing at outside of a controller action and you cannot handle the error. Instead, you will see the 500 error page if it's in development mode.

Field Error Binding

By default, this plugin will bind a field error to an instance with a message code default.optimistic.locking.failure. If you don't want it because i18n message is unnecessary or you want to use your custom message code, you can disable it by errorBinding parameter:

sampleDomain.withOptimisticLock(modificationBaseVersion, [errorBinding: false]) { Object domain ->
    …
}

If modificationBaseVersion is unnecessary, you can simply write it:

sampleDomain.withOptimisticLock(errorBinding: false) { Object domain ->
    …
}

If you want use your custom message code, you can use Spring's API directly like this:

sampleDomain.withOptimisticLock(errorBinding: false) { Object domain ->
    …
}.onConflict { Object domain ->
    domain.errors.rejectValue("your.custom.code", [123] as Object[], "Default sample")
}

The errorBinding parameter is true by default. So if you don't specify the parameter, the default field error will be bound. You can configure the default value in Config.groovy:

grails.plugins.domainlocking.defaultErrorBinding = false

3 Pessimistic Locking

In order to operate a domain instance on multi-threads sequentially, Grails provides lock method on domain class. I know its usage is very easy, but the plugin provides the wrapper way for withPessimisticLock in the point of view of symmetry for withOptimisticLock.
  • withPessimisticLock static method invoke the closure with the locked domain instance specified by id.
  • If target row isn't found, onNotFound closure is invoked.

SampleDomain.withPessimisticLock(id) { Object lockedDomain ->

// operation to require a pessimistic lock

}.onNotFound {domainId ->

// operation when the target is not found }

See also Optimistic and Pessimistic locking and lock method of domain class.

onNotFound is optional

If you don't have nothing to do on target not found, you can omit onNotFound.

SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
    // …
}

In this case, when target isn't found, all the plugin has to do is nothing.

Closure arguments

The lockedDomain argument of withPessimisticLock's closure is the domain instance which is found by SampleDomain.lock(id) method.

SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
    assert lockedDomain.id == id
}

The domainId argument of onNotFound's closure is the domain id which is specified to withPessimisticLock's first argument.

SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
    // …
}.onNotFound {domainId ->
    assert domainId == id
}

Return value

If you want a return value from closure, you can receive it via returnValue property.

def result = SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
    return "OK"
}.onNotFound { ->
    return "NG"
}
assert result.returnValue == "OK"

In case that no row is found, a return value of onNotFound's closure is returned.

assert result.returnValue == "NG"

Transaction is required

When you want to use a pessimistic lock, you must need a transaction. The aquired lock is automatically released when the transaction commits.