Reusing grails Criteria for multiple domains using Closure.delegate
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.
[code language=”groovy”]class Subscription {
static belongsTo = [topic: Topic]
}
class Resource {
static belongsTo = [topic: Topic]
}
class Topic {
static hasMany = [resources: Resource, subscriptions: Subscription]
}[/code]
I was trying to get the count of resources grouped by topic ids. So I wrote the following method.
[code language=”groovy”]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
}[/code]
I also needed to get the count of subscriptions grouped by topic ids. So I wrote the following method.
[code language=”groovy”]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
}[/code]
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
[code language=”groovy”]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
}[/code]
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.
[code language=”groovy”]List properties = createCriteria() //Rest of it[/code]
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
[code language=”groovy”]//delegate was set to Resource before this Closure was called so
//delegate = Resource
List properties = delegate.createCriteria() //Rest of it[/code]
As Resource is a domain class Resource.createCriteria() will be found.
In case we are finding subscriptions it will execute like this
[code language=”groovy”]//delegate was set to Subscription before this Closure was called so
//delegate = Subscription
List properties = delegate.createCriteria() //Rest of it[/code]
As Subscription is a domain class Subscription.createCriteria() will be found.
References
Turns out I wasn’t entirely wrong; Burt explains in this blog post how to define reusable criteria closures to be used as namedQueries in multiple domain classes:
Oops, I missed the “multiple domains” part; namedQueries won’t help there 🙂 Thanks for the tip!
If you want to reuse the criteria only for querying (and not deleteAll() for example), you could use ‘namedQueries’: http://grails.github.io/grails-doc/latest/ref/Domain%20Classes/namedQueries.html
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!