Reusing grails Criteria for multiple domains using Closure.delegate

12 / Apr / 2015 by Aseem Bansal 4 comments

I recently had a situation where I had the exact same criteria in multiple domains. I found a way to DRY them using Closure.delegate. I wanted to share that in this post.

Just including the relevant details the domains that I had were like the following.

class Subscription {
   static belongsTo = [topic: Topic]
}
class Resource {
   static belongsTo = [topic: Topic]
}
class Topic {
   static hasMany = [resources: Resource, subscriptions: Subscription]
}

I was trying to get the count of resources grouped by topic ids. So I wrote the following method.

def Map getNumberOfResourcesForTopicIds(List<Long> topicIds) {
   List resources = Resource.createCriteria().list{
      createAlias('topic', 't')
      projections {
         groupProperty('t.id')
         rowCount()
      }
      'in' 't.id', topicIds
   }
   //... Rest of code to get Map from List
}

I also needed to get the count of subscriptions grouped by topic ids. So I wrote the following method.

def Map getNumberOfSubscriptionsForTopicIds(List<Long> topicIds) {
   List subscriptions = Subscription.createCriteria().list{
      createAlias('topic', 't')
      projections {
         groupProperty('t.id')
         rowCount()
      }
      'in' 't.id', topicIds
   }
   //... Rest of code to get Map from List
}

As can be seen these are exactly same criterias. But as these are on different domains I didn’t know any simple way to reuse them. After a bit of search I came across this question.

So I refactored the above two methods to the following

def Map getNumberOfSubscriptionsForTopicIds(List<Long> topicIds) {
    getNumberOfPropertyMappedByTopicIds.delegate = Subscription
    getNumberOfPropertyMappedByTopicIds(topicIds)
}

def Map getNumberOfResourcesForTopicIds(List<Long> topicIds) {
    getNumberOfPropertyMappedByTopicIds.delegate = Resource
    getNumberOfPropertyMappedByTopicIds(topicIds)
}

private def getNumberOfPropertyMappedByTopicIds = {List<Long> topicIds ->
   List properties = createCriteria().list{
      createAlias('topic', 't')
      projections {
         groupProperty('t.id')
         rowCount()
      }
      'in' 't.id', topicIds
   }
   //... Rest of code to get Map from List
}

Now for the above refactoring most people would ask what was done and how is it working? So let’s start with the explanation.

In groovy every closure has a delegate. The delegate is something on which the closure can offload its work. Meaning that in the closure if there is a method/property that the closure cannot find then it will ask its delegate to find that method/property.

Like in case of the Closure getNumberOfPropertyMappedByTopicIds what is the method createCriteria() ? The closure has no idea what it is because that method is not present in service in which this closure has been defined. So when the closure is called normally then we should expect a MissingMethodException.

But in this case we are not calling this closure normally. In each of the methods we are setting the delegate before calling the closure.  So when createCriteria() is executed then firstly closure will try to find the method in the service in which it was defined.

List properties = createCriteria() //Rest of it

It will not be able to find createCriteria() in the service so next it will ask its delegate.

In case we are finding resources it will execute like this

//delegate was set to Resource before this Closure was called so
//delegate = Resource
List properties = delegate.createCriteria() //Rest of it

As Resource is a domain class Resource.createCriteria() will be found.

In case we are finding subscriptions it will execute like this

//delegate was set to Subscription before this Closure was called so
//delegate = Subscription
List properties = delegate.createCriteria() //Rest of it

As Subscription is a domain class Subscription.createCriteria() will be found.

References

FOUND THIS USEFUL? SHARE IT

comments (4)

  1. Alberto Vilches

    Hi, nice trick but pay attention because assigning the delegate of the getNumberOfPropertyMappedByTopicIds closure is not thread safe because is a property of your class. Maybe it will be better if you clone it before, just the DefaultGroovyMethods.with(…) method works (from Groovy source code). Cheers!

    Reply

Leave a comment -