Domain Locking - 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.
2 Optimistic Locking
In order to handle optimistic locking, usewithOptimisticLock method which is injected to domain class by the plugin.
withOptimisticLockmethod compares a version of modification base with current version.withOptimisticLock's closure is invoked only when "version of modification base" < "current persistent version" istrue.- When "version of modification base" < "current persistent version" is
falseorOptimisticLockingFailureExceptionis catched, - it invokes
onConflictclosure if exists. - it sets a field error message to the domain instance.
- When an exception except
OptimisticLockingFailureExceptionis 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
}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 themodificationBaseVersion argument as follows:sampleDomain.withOptimisticLock { Object domain ->
// Here is always invoked regardless of the versions.
}onConflict is optional
If you have nothing to do when a conflict occurs, you can omitonConflict closure:sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
// …
}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)
}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 fromreturnValue property.def result = sampleDomain.withOptimisticLock(modificationBaseVersion) { Object domain ->
return "OK"
}.onConflict { Object domain, Throwable caused ->
return "NG"
}
assert result.returnValue == "OK"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.
}Field Error Binding
By default, this plugin will bind a field error to an instance with a message codedefault.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 -> … }
modificationBaseVersion is unnecessary, you can simply write it:sampleDomain.withOptimisticLock(errorBinding: false) { Object domain -> … }
sampleDomain.withOptimisticLock(errorBinding: false) { Object domain -> … }.onConflict { Object domain -> domain.errors.rejectValue("your.custom.code", [123] as Object[], "Default sample") }
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 = false3 Pessimistic Locking
In order to operate a domain instance on multi-threads sequentially, Grails provideslock 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.
withPessimisticLockstatic method invoke the closure with the locked domain instance specified byid.- If target row isn't found,
onNotFoundclosure is invoked.
SampleDomain.withPessimisticLock(id) { Object lockedDomain -> // operation to require a pessimistic lock}.onNotFound {domainId -> // operation when the target is not found
}onNotFound is optional
If you don't have nothing to do on target not found, you can omitonNotFound.SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
// …
}Closure arguments
ThelockedDomain 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
}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 viareturnValue property.def result = SampleDomain.withPessimisticLock(id) { Object lockedDomain ->
return "OK"
}.onNotFound { ->
return "NG"
}
assert result.returnValue == "OK"onNotFound's closure is returned.
assert result.returnValue == "NG"