DRYing Grails Criteria Queries

by Geoff Lane on September 2nd, 2009

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?

From → Code, Groovy, Web

10 Comments
  1. That’s a great post.

    In Grails 1.2-m2, we’ve got named queries introduced in grails core giving named queries ‘methods’ in domain classes.

    Thanks a lot.

    []s,

  2. Gregg Bolinger permalink

    Good info. But what happens if you need to execute a query that doesn’t need the eq(‘published’, true) ? All withCriteria calls are monkey patched at this point.

  3. Good idea.

    There are many ways to skin a cat. Here’s another one that’s good for wildcard queries (‘%’). Of course you should validate the data first.

    def user_params = ['slug', 'category']
     
    def articles = Article.withCriteria {
      for (e in params) {
        if (e.key in user_params) {
            ilike(e.key, '%' + e.value + '%')
        }
      }
    }
  4. @Gregg, Monkeypatching it like that makes it available to be used, but doesn’t automatically apply it to all criteria. So if you want to find all approved and not approved you just do something like:

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

    Without the approved {} block.

  5. @Tom,
    Nice example, thanks for sharing.

  6. Gregg Bolinger permalink

    Ahh, I missed the published closure. Excellent. Thanks.

  7. Hi Geoff,

    One other thing that can be done to Hibernate Criteria Builder is adding some methods that will add the actual restriction only if searched value is not null/empty.

    You can check the code from my post here: http://www.aqris.com/x/NoBx

  8. Kyle Dickerson permalink

    I came up with a more flexible way that doesn’t require monkey-patching the metaclass. It involves currying the CriteriaBuilder object to separate closures that you want to combine into a single query. I wrote it up on my blog, you can read it there: http://dickersonshypotheticalblog.blogspot.com/2009/10/grails-dynamically-building-criteria.html

  9. Peter Willis permalink
    def c = Domain.createCriteria()
     
    def q0 = {
     eq 'name', 'peter'
    }
     
    def q1 = {
     eq 'approved', true
    }
     
    def q2 = {
     le 'time', new Date() - 7
    }
     
    Closure join(Object[] queries) {    
        return {
            for(q in queries) {
                q.delegate = delegate
                q()
            }
        }
    }
     
    c.list( join(q0, q1, q2) )
  10. Thanks man, you saved my ass bigtime :)

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS