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
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.
2 Optimistic Locking
In order to handle optimistic locking, usewithOptimisticLock
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" istrue
.- When "version of modification base" < "current persistent version" is
false
orOptimisticLockingFailureException
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
}
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 = false
3 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
.
withPessimisticLock
static method invoke the closure with the locked domain instance specified byid
.- 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
}
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"