Hi Gianluca,
Thank you - this is exactly the pattern I was hoping to see laid out
end-to-end. A few thoughts and one follow-up question.
1) Your shape is effectively the real-world case for PR #15447
(apache/grails-core):
def query = TWorkAssignment.where {}
if (filterParams.containsKey('id')) query = query.where { id ==
filterParams.id }
if (filterParams.containsKey('workPackage')) query = query.where {
workPackage.id == filterParams.workPackage }
Prior to that fix, DetachedCriteriaTransformer only visited
DeclarationExpression, so every query = query.where { ... } re-assignment was
silently skipped by the AST transform. At runtime the untransformed closures
evaluated to non-mutating clones and buildQuery would have returned every
TWorkAssignment regardless of filterParams. Adding visitBinaryExpression fixed
that. Your reply confirms the fix covers the canonical "empty initial where +
chained reassignment under if-guards" idiom, which is good to see.
2) One specific question on the alias line:
if (filterParams.containsKey('workPackage.code')) query = query.where {
def wp = workPackage
wp.code == filterParams.'workPackage.code'
}
The GORM reference documents the explicit-alias idiom (def wp = workPackage)
primarily for sorting/projection (.list(sort: 'wp.code')), but you are using it
here purely for a filter. Is there a specific case where the direct form:
workPackage.code == filterParams.'workPackage.code'
misbehaves, and the aliased form rescues it? Or is this stylistic / defensive?
If you have hit a real transformer or scope issue with the direct form, would
you be willing to open an issue on apache/grails-core (or a PR if you already
have a fix in mind)? It would be useful to track it alongside #15447/#15448
rather than leave it as tribal knowledge.
If it is stylistic rather than a bug, would you consider contributing this
pattern directly to the Grails documentation - specifically the GORM "Where
Queries" section
(https://grails.apache.org/docs/latest/guide/GORM.html#whereQueries)? The
existing docs cover basic property comparison and associations but do not
describe composing conditional filters via chained re-assignment, the
flat-map-with-dotted-keys convention, or the aliased form for nested-property
filtering. From your Medium article and this reply, the rules that emerge are:
- @CompileStatic on the class, @CompileDynamic on the buildQuery method
- Accept a Map filterParams; do not name local variables after domain
properties
- Start with an empty where {} and chain conditional query = query.where {
... } re-assignments
- Flat-map-with-dotted-keys for nested filters:
filterParams.'workPackage.code'
- FK shortcut for simple association filters: workPackage.id ==
filterParams.workPackage
- Aliased association access for nested-property filters: def wp =
workPackage; wp.code == ...
James
On 2026/04/20 10:42:29 Gianluca Sartori wrote:
> Hi James,
>
> this is the query:
>
> @CompileDynamic
> private DetachedCriteria<TWorkAssignment> buildQuery(Map filterParams) {
> def query = TWorkAssignment.where {}
>
> if (filterParams.containsKey('id')) query = query.where { id
> == filterParams.id }
> if (filterParams.containsKey('workPackage')) query =
> query.where { workPackage.id == filterParams.workPackage }
> if (filterParams.containsKey('workPackage.code')) query = query.where
> {
> def wp = workPackage
> wp.code == filterParams.'workPackage.code'
> }
>
> return query
> }
>
>
> Thank you for your time,
> Gianluca
>
> Gianluca Sartori
> --
> https://dueuno.com
>
>
> On Sat, 18 Apr 2026 at 02:43, James Fredley <[email protected]> wrote:
>
> > Can you share buildQuery? The def o1 = owner AST trick requires the
> > literal Domain.where { ... } form; if your builder composes criteria
> > programmatically, the alias won't be registered and the sort will silently
> > drop.
> >
> > James
> >
> > On 2026/04/16 13:31:10 Gianluca Sartori wrote:
> > > Hi folks,
> > >
> > > do you have evidence that the where query aliases are working for
> > sorting?
> > > It does not seem to work in my code.
> > >
> > > The only difference I see is that I sort multiple columns with something
> > > like:
> > >
> > > def query = buildQuery(filterParams)
> > > return query.list(sort: ['o1.firstName': 'desc'])
> > >
> > >
> > >
> > > >>>
> > > 7.4.6. Query Aliases and Sorting
> > >
> > > If you define a query for an association an alias is automatically
> > > generated for the query. For example the following query:
> > >
> > > def query = Pet.where {
> > > owner.firstName == "Fred"
> > > }
> > >
> > > Will generate an alias for the owner association such as owner_alias_0.
> > > These generated aliases are fine for most cases, but are not useful if
> > you
> > > want to later sort or use a projection on the results. For example the
> > > following query will fail:
> > >
> > > // fails because a dynamic alias is used
> > > Pet.where {
> > > owner.firstName == "Fred"
> > > }.list(sort:"owner.lastName")
> > >
> > > If you plan to sort the results then an explicit alias should be used and
> > > these can be defined by simply declaring a variable in the where query:
> > >
> > > def query = Pet.where {
> > > def o1 = owner
> > > o1.firstName == "Fred"
> > > }.list(sort:'o1.lastName')
> > >
> > > Define an alias called o1
> > > Use the alias in the query itself
> > > Use the alias to sort the results
> > >
> > > By assigning the name of an association to a local variable it will
> > > automatically become an alias usable within the query itself and also for
> > > the purposes of sorting or projecting the results.
> > >
> > >
> > >
> > > Gianluca Sartori
> > > --
> > > https://dueuno.com
> > >
> >
>