PWFraley
PWFraley

Reputation: 1082

Adding Dynamic Fields to Domain Object in Grails

I am trying to find a way to add dynamic fields to a grails domain class. I did find the dynamic domain class plugin based on Burt's article, but this is way too much for our needs.

Supposed we have a domain class of person:

class Person extends DynamicExtendableDomainObject {
    String firstName
    String lastName

    static constraints = {
        firstName(nullable: false, blank: false, maxSize: 50)
        lastName(nullable: false, blank: false)
    }
}

Now customer a wants to also have a birthdate field in this. By using some sort of management tool, he adds this extra field in the database.

Customer b wants to also have a field middle name, so he is adding the field middle name to the person.

Now we implemented a DynamicExtendableDomainObject class, which the Person class inherits from. This adds a custom field to each Domain class inheriting from this to store the dynamic properties as JSON in it (kind of like KiokuDB in Perl stores them).

Now when Person is instantiated, we would like to add those dynamic properties to the Person class, to be able to use the standard Grails getter and setter as well as Templating functions for those.

So on customer a we could use the scaffolding and person would output firstName, lastName, birthDate, on customer b the scaffolding would output firstName, lastName, middleName.

The storing of the properties will be implemented by using the saveinterceptor, to serialize those properties to JSON and store them in the special field.

But we have not yet found a way to add these JSON properties dynamically to the domain class during runtime. Is there a good way to handle this? And if so, how to best implement this?

Upvotes: 1

Views: 3019

Answers (1)

Torben Rauche
Torben Rauche

Reputation: 31

You can try to add the properties at runtime to the DomainClass of type DynamicExtendableDomainObject by expanding getProperty(), setProperty(), setProperties() in the metaClass and then use beforeUpdate(), beforeInsert() and afterLoad() to hook into Persistence.

For example in Bootstrap (or service):

def yourDynamicFieldDefinitionService

for(GrailsClass c in grailsApplication.getDomainClasses()){
    if(DynamicExtendableDomainObject.isAssignableFrom(c.clazz)){
        Set extendedFields = yourDynamicFieldDefinitionService.getFieldsFor(c.clazz)

        //getProperty()
        c.clazz.metaClass.getProperty = { String propertyName ->
            def result
            if(extendedFields.contains(propertyName)){
                result = delegate.getExtendedField(propertyName)
            } else {
                def metaProperty = c.clazz.metaClass.getMetaProperty(propertyName)
                if(metaProperty) result = metaProperty.getProperty(delegate)
            }
            result
        }

        //setProperty()
        c.clazz.metaClass.setProperty = { propertyName , propertyValue ->
                    if(extendedFields.contains(propertyName)){
                        delegate.setExtendedField(propertyName, propertyValue)
                        delegate.blobVersionNumber += 1
                    } else {
                        def metaProperty = c.clazz.metaClass.getMetaProperty(propertyName)
                        if(metaProperty) metaProperty.setProperty(delegate, propertyValue)
                    }
                }

        //setProperties()
                def origSetProperties = c.clazz.metaClass.getMetaMethod('setProperties',List)
                c.clazz.metaClass.setProperties = { def properties ->
                    for(String fieldName in extendedFields){
                        if(properties."${fieldName}"){
                            delegate."${fieldName}" = properties."${fieldName}"
                        }
                    }
                    origSetProperties.invoke(delegate,properties)
                }
    }
}

with

abstract DynamicExtendableDomainObject {
    String yourBlobField
    Long blobVersionNumber //field to signal hibernate that the instance is 'dirty'

    Object getExtendedField(String fieldName){
        ...
    }

    void setExtendedField(String fieldName, Object value){
        ...
    }

    def afterLoad(){
        //fill your transient storage to support getExtendedField + setExtendedField 
    }

    def beforeUpdate(){
        //serialize your transient storage to yourBlobField
    }

    def beforeInsert(){
        //serialize your transient storage to yourBlobField
    }
}

Upvotes: 2

Related Questions