Assigning ID for domain objects in Grails via constructor
Update for Grails 2.2+
As of Grails 2.2-RC1 it is possible to simply add a bindable:true
to the constraints
section of the domain class to
allow assignment in the constructor / findOrCreateWhere:
class MyDomain {
static constraints = {
// allow binding of "id" attribute (e.g. in constructor or url parameters)
id bindable: true
}
}
The Problem
I’m currently writing a Grails application with many domain objects that use a id generator of assigned
:
class MyDomain {
String name
static mapping = {
id generator: 'assigned'
}
}
For domain objects of this kind, the id
property must be set manually before any save()
is possible. Unfortunately
Grails doesn’t allow to set the id property via a map to the constructor, although this works for any other property:
// doesn't work for the "id" property!
domain = new MyDomain(id: 123, name: "Test")
// doesn't work either (for the "id" property)
domain = MyDomain.findOrCreateWhere(id: 123, name: "Test")
A manual assignment of the id outside of the constructor actually works:
domain.id = 123
I don’t know why this is the case, however there are bug reports that seem to be caused by
this: GRAILS-1984
and GRAILS-8422. As a result any scaffold’ed Controller won’t be able to
create new domain objects, as it uses the constructor internally to assign all fields. As this doesn’t work for the id
, the object is not savable and creation fails.
A (temporary) solution
Until the described issues are fixed, it is possible to override the constructor of all domain classes to accept
the id
property. I don’t know the internals of Grails and this might introduce some security holes to your
application. However, I’m not aware of any AND this behavior is really annoying, so I’m willing to take the risk (“kids,
don’t try this at home!”).
By adding the following code to your BootStrap
class, all domain classes are modified with a “fixed” constructor:
class BootStrap {
def grailsApplication
def init = { servletContext ->
grailsApplication.domainClasses.each { clazz ->
def oldConstructor = clazz.metaClass.retrieveConstructor(Map)
clazz.metaClass.constructor = { Map data ->
def instance = oldConstructor.newInstance(data)
def idName = clazz.identifier.name
if (data.containsKey(idName)) {
def unparsedValue = data."$idName"
def value
if (unparsedValue == null || unparsedValue == "")
value = null
else
value = clazz.identifier.type.valueOf(unparsedValue)
instance."$idName" = value
}
return instance
}
}
}
}
It overrides the Map
constructor of each domain class and calls the original one first. After this, the identity
property is set if specified.
After applying this change, the scaffolded Controllers are working and findOrSaveWhere()
/ findOrCreateWhere()
are
functional, too.