DRYing Grails Criteria Queries

September 2, 2009 - 5 minute read -
groovy meta-programming grails DRY

When you're writing code, Don't Repeat Yourself. Now say that 5 times. *rimshot*

One of the things that I find myself repeating a lot of in many business apps is queries. It's common to have a rule or filter that applies to many different cases. I came across such a situation recently and wanted to figure out a way to share that filter across many different queries. This is what I came up with for keeping those Criteria DRY.

To start with, I'll use an example of an Article. This could be a blog post or a newspaper article. One of the rules of the system is that Articles need to be published before they are visible by end users. Because of this seemingly simple rule, every time we query for Articles, we will need to check the published flag. If you get a lot of queries, that ends up being a lot of repetition.

Here's our example domain class:

package net.zorched.domain
class Article {
    String name
    String slug
    String category
    boolean published
    static constraints = {
        name(blank: false)
        slug(nullable: true)
    }
}

Now we need to add a query that will retrieve our domain instance by its slug (a slug is a publishing term for a short name given to an article, in the web world it has become a term often used for a search engine optimization technique that uses the title instead of an artificial ID). To perform that query we might write something like this on the Article class:

    static getBySlug(String slug) {
        withCriteria(uniqueResult:true) {
            and {
                eq('approved', true)
                eq(' slug',  slug)
            }
        }
    }

We want to query based on the slug, but we also want to only allow a published Article to be shown. This would allow us to unpublish an article if necessary. Without the approved filter, if the link had gotten out, people could still view the article.

Next we decide we want to list all of the Articles in a particular category so we write something like this, again filtering by the approved flag.

    static findAllByCategory(String category) {
        withCriteria() {
            and {
                eq('approved', true)
                eq('category',  category)
            }
        }
    }

Two simple examples like this might not be that big of a deal. But you can easily see how this would grow if you added more custom queries or if you had some more complicated filtering logic. Another common case would be if you had the same filter across many different domain objects. (What if the Article had attachments and comments all of which needed their own approval?) What you need is a way to share that logic among multiple withCriteria calls.

The trick to this is understanding how withCriteria and createCriteria work in GORM. They are both implemented using a custom class called HibernateCriteriaBuilder. That class invokes the closures that you pass to it on itself. Sounds confusing. Basically the elements in the closure of your criteria queries get executed as if the were called on an instance of HibernateCriteriaBuilder.

e.g.

withCriteria {
     eq('a', 1)
     like('b', '%foo%')
}

would be the equivalent of calling something like:

def builder = new HibernateCriteriaBuilder(...)
builder.eq('a', 1)
builder.like('b', '%foo%')

That little bit of knowledge allow you to reach into your meta programming bag of tricks and add new calls to the HibernateCriteriaBuilder. Every Class in groovy has a metaClass that is used to extend types of that Class. In this case we'll add a Closure that will combine our criteria with other criteria like so:

HibernateCriteriaBuilder.metaClass.published = { Closure c ->
    and {
        eq('published', true)
        c()
    }
}

This ands together our eq call with all of the other parts of the passed in closure. Now we can put the whole thing together into a domain class with a reusable filter.

package net.zorched.domain
import grails.orm.HibernateCriteriaBuilder
class Article {
    static {
        // monkey patch HibernateCriteriaBuilder to have a reusable 'published' filter
        HibernateCriteriaBuilder.metaClass.published = { Closure c ->
            and {
                eq('published', true)
                c()
            }
        }
    }
    String name
    String slug
    String category
    boolean published
    Date datePublished
    def publish() {
        published = true
        datePublished = new Date()
    }
    static def createSlug(n) {
        return n.replaceAll('[^A-Za-z0-9\\s]','')
                 .replaceAll('\\s','-')
                 .toLowerCase()
    }
    static findAllApprovedByCategory(String category) {
        withCriteria {
            published {
                eq('category', category)
            }
        }
    }
    static getBySlug(String slug) {
        withCriteria(uniqueResult:true) {
            published {
                eq(' slug',  slug)
            }
        }
    }
    static constraints = {
        name(blank: false)
        datePublished(nullable: true)
        slug(nullable: true)
    }
}

And there you have it. Do you have any other techniques that can be used to DRY criteria?