4

EDIT1: I also accept answers that guide me in reusing DetachedCriteria without where queries. Where queries are my preference, but if regular DetachedCriteria is less hassle and more mature, I am willing to use it instead.

I've been trying to wrap my head around 'where' queries in Grails for some time now. At first, I was amazed by how powerful they are: I can build queries without leaving my business logic layer. I thought they were a solution for some performance issues I was facing with complex actions that require a lot of transactional logic.

However, I feel like the documentation omits all the problems with where queries. I have no idea how to use them in my application, although I have been reading about them and playing around with them a lot. Here is how I want to use them:

In my application, there are a lot of subqueries that are redundant when using HQL or native SQL, but I cannot reuse them, because I don't want them all be executed after another. I want them to be executed in one database query.

As far as I'm concerned, DetachedCriteria or where-queries are the way to go for me. I decided to use where queries, since I like the approach and syntax much better than regular criteria.

Here is the issue: In many variations, my where queries are ignored! Depending on the situation, I have to memorize a certain order or structure.

Reuse DetachedCriteria Objects

I think the most logical thing a developer would try to do is encapsulate often used where queries and access them when desired. So, I put some where queries in service methods as so:

DetachedCriteria<Customer> allCustomersForProjektCriteria(DetachedCriteria<Customer> criteria, Project project){
    criteria.where{projekt == project}
}

I like that where queries return objects of DetachedCriteria. Theoretically. Maybe it is just syntactic sugar, I don't know. I tried to do something like that and the behaviour feels weird, to say the least.

someService.allCustomersForProjectCriteria(Customer.where{name != null}, Project.get(1))
        .where{representatives {type != 'Manager'}}
        .list()

The first where query as well as the one in my service method work fine, but the last one does not. That is how I found out that you are only allowed to chain where queries within the same method or closure. But is that the expected behaviour? A similar approach can also be found in the grails documentation under "Query Composition".

What is the use case where I put all my where queries in one method? And how can it be that DetachedCriteria offers the method DetachedCriteria where(Closure) if it is being ignored in some cases silently? Why is no exception thrown?

Reuse Where Query Closures

My next approach is also mentioned in the same part of the grails documentation as mentioned above. Rather than chaining together where queries that are encapsulated in different methods, I tried to encapsulate just the closures to reuse it at will. I tried the following:

Closure allCustomersForProjectCriteria(Project proj){
    ({project == proj} as DetachedCriteria<Customer>)
}

Then use it like that:

Customer.where(allCustomersForProjectCriteria(Projekt.get(1))
        .where{name != null && representatives {type != 'Manager'}}
        .list()

Now that fails with the following Stacktrace:

org.codehaus.groovy.runtime.typehandling.GroovyCastException: Error casting closure to grails.gorm.DetachedCriteria, Reason: null

    at org.springsource.loaded.ri.ReflectiveInterceptor.jlrMethodInvoke(ReflectiveInterceptor.java:1276)

    at ConsoleScript148$_run_closure2.doCall(ConsoleScript148:9)

    at org.springsource.loaded.ri.ReflectiveInterceptor.jlrMethodInvoke(ReflectiveInterceptor.java:1276)

    at ConsoleScript148.run(ConsoleScript148:12)

    at org.springsource.loaded.ri.ReflectiveInterceptor.jlrMethodInvoke(ReflectiveInterceptor.java:1276)

I played around a little and found out that I cannot declare the closure, cast it and return it in one line of code. I had to rewrite my method as follows:

Closure allCustomersForProjectCriteria(Project proj){
    def criteria = {project == proj} as DetachedCriteria<Customer>
    criteria
}

This is a minor annoyance, but I could easily live with it. I thought I got it all figured out now, because no exception was raised. But, again, only the where query from my method was executed, the other one was ignored. That was the point where my confusion reached its peak. What is happening? That does not feel consistent. When can I trust my criteria to be considered if it is ignored sometimes without an exception?

Weirdly enough, if I put my second where query in a closure, it works again!!

    def additionalCriteria = {name != null && representatives {type != 'Manager'}} as DetachedCriteria<Customer>
    Customer.where(allCustomersForProjectCriteria(Projekt.get(1))
        .where(additionalCriteria)
        .list()

Now to get to the point...

So this is how I try to make sense of it: I can chain multiple where queries with a newly defined closure after each other if, and only if, they are located in the same method or closure.

def results = Customer.where{...}
        .where{...}
        .list()

If they are in different methods, all but the first where query are ignored:

def getQuery(){
    Customer.where{...}
}
query.where{ ... } //This one is ignored

The first one was returned from a method or closure, so it is not a 'normal' where query anymore, it is a 'returned where query'. Closure definition at method call does not work now, and I have to use it like that:

def getQuery(){Customer.where{...}}
def myWhereClosure = { ... }
query.where(myWhereClosure) //This one is NOT ignored anymore!

If I encapsulate the closures for my queries, it is the same; as soon as I execute one where query with an encapsulated where closure as a parameter, it is now a 'returned where query' and cannot be used regularly anymore:

def getCustomerCrit(){
    def clos = {...} as DetachedCriteria<Customer>
    clos 
}
def results = Customer.where(customerCrit)
        .where{...} //This one is ignored
        .list()  

So, to make it work I use a predefined closure for the second where query, so that it is also a 'returned where query' and is not ignored:

...

def additionalCrit = {...}
def results = Customer.where(customerCrit)
        .where(additionalCrit) //This one is NOT ignored anymore
        .list()  

So what is the intended way to use these queries, how do they work under the hood and where can I find more resources on that topic than in the documentation?

Community
  • 1
  • 1
nst1nctz
  • 333
  • 3
  • 23
  • 1
    Thanks for the useful summary. Yes, this should all be documented better and I suspect some of it might need rewriting to fix these anomolies. – xpusostomos Apr 28 '21 at 12:40

0 Answers0