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