Skip to main content

redcube.de

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.