Monday, February 26, 2007

Getting Dynamic with Grails' ExpandoMetaClass



In Grails 0.4 the ExpandoMetaClass was introduced that allows you to dynamically add methods, constructors, properties and static methods using a neat closure syntax.

A posting on the Grails User Forum today (see http://www.nabble.com/Newbie---Need-to-implement-a-soft-) gave me a perfect example to explain and show the power of what is possible with this ExpandoMetaClass.

The question on the forum was how the dynamic delete method could be overridden by a 'soft' delete. This meaning a deleted flag set to true and the record to be saved.

In this example I'm not implementing this exact functionality. It just gave me the idea for this post. Imagine you never want to delete records from your databse, but always want just want to mark them. For example to implement some Recycle Bin functionality in which you can later restore records or delete them forever.


What you would need is to define a dynamic "softDelete()" method for all domain classes which sets the deleted flag and updates the record. Actually with the ExpandoMetaClass it's so simple.

In the init closure of the ApplicationBootStrap.groovy class you can do some bootstrapping for your application. This bootstrap class is often used to insert some "demo" records. For example the simple-cms sample application included in the Grails distribution does this. But wihtin this bootstrap class you also use the ExpandoMetaClass to define dynamic methods. See the code below:



import org.codehaus.groovy.grails.commons.metaclass.*
import org.codehaus.groovy.grails.commons.GrailsApplication

class ApplicationBootStrap {

def init = { servletContext ->

// get application
def application = servletContext.getAttribute(GrailsApplication.APPLICATION_ID)

// define softDelete() method for all domain classes
application.domainClasses.each { dc ->
dc.metaClass.softDelete << { ->
delegate.deleted = true
delegate.save()
}
}
}
def destroy = {
}
}

Note that the code above assumes that all domain classes have a "deleted" boolean property. This could be added to all domain classes or perhaps been added dynamically. But it's out of scope for this example.
To check if it's working you can do some test like:


// create new book
new Book(title:'The Definitive Guide to Grails').save()

// retrieve saved book
def savedBook = Book.get(1)
assert savedBook.deleted == false

// now softDelete the book
savedBook.softDelete()

// retrieve it again
def softDeletedBook = Book.get(1)
assert savedBook.deleted == true

7 comments:

Anonymous said...

Marcel,

Thanks for this post, it has opened up some interesting possibilities for me. What would be the syntax when the expando method takes parameters:

dc.metaClass.findTemplate << { name -> dc.name = name }

??

Thanks,
John

Anonymous said...

oops i think i meant:

dc.metaClass.findTemplate << { name -> delegate.name = name }

i was focused on the parameter part

Marcel Overdijk said...

Hi John,

You can add a name parameter like:

dc.metaClass.findTemplate << { String name -> ..your code goes here.. }

Cheers,
Marcel

james_027 said...

hi,

I am just trying to learn about ExpandoMetaClass, your article is a great help. I wonder why I didn't see something like metaClass = new ExpandoMetaClass(it) in your article? Is this lacking or is there something I don't understand?

Thanks,
james

Marcel Overdijk said...

Hi James,

The domainClass has already a metaClass assigned to it. So there is no need to create a new ExpandoMetaClass instance. Just use its metaClass directly by dc.metaClass.xxx << { -> [your code] }

Cheers,
Marcel

Blacktiger said...

Marcel,

This new method won't do cascading deletes right? How would you do a cascading delete?

shubhatk said...

Marcel,
This post was very helpful,the app works fine with softDelete but the BookTests.groovy fails with this error "Running test BookTests...PASSED
java.lang.NullPointerException: Cannot get property 'domainClasses' on null object
at org.codehaus.groovy.runtime.NullObject.getProperty(NullObject.java:56)
at org.codehaus.groovy.runtime.InvokerHelper.getProperty(InvokerHelper.java:153)
at org.codehaus.groovy.runtime.callsite.NullCallSite.getProperty(NullCallSite.java:29)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callGetProperty(AbstractCallSite.java:236)
at BootStrap$_closure1.doCall(BootStrap.groovy:9)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597) ....."
Please advice.
Thanks,
Shubha