Build createCriteria in Grails dynamically and in a DRY way?
Asked Answered
E

1

6

I'm working on building a createCriteria dynamically. So far, so good:

obj is the domain object(s) I want back

rulesList is a list of maps which hold the field to be searched on, the operator to use, and the value to search against

def c = obj.createCriteria()
l = c.list (max: irows, offset: offset) {
    switch(obj){           //constrain results to those relevant to the user
        case Vehicle:
            eq("garage", usersGarage)
            break
        case Garage:
            users {
                idEq(user.id)
            }
            break
    }
    rulesList.each { rule ->
        switch(rule['op']){
            case 'eq':
                eq("${rule['field']}", rule['value'])
                break
            case 'ne':
                ne("${rule['field']}", rule['value'])
                break
            case 'gt':
                gt("${rule['field']}", rule['value'])
                break;
            case 'ge':
                ge("${rule['field']}", rule['value'])
                break
            case 'lt':
                lt("${rule['field']}", rule['value'])
                break
            case 'le':
                le("${rule['field']}", rule['value'])
                break
            case 'bw':
                ilike("${rule['field']}", "${rule['value']}%")
                break
            case 'bn':
                not{ilike("${rule['field']}", "${rule['value']}%")}
                break
            case 'ew':
                ilike("${rule['field']}", "%${rule['value']}")
                break
            case 'en':
                not{ilike("${rule['field']}", "%${rule['value']}")}
                break
            case 'cn':
                ilike("${rule['field']}", "%${rule['value']}%")
                break
            case 'nc':
                not{ilike("${rule['field']}", "%${rule['value']}%")}
                break
            }
        }
    }
}

The above code works fine and is only a little verbose-looking with the switch statements. But what if I want to add functionality to choose to match ANY of the rules or ALL of them? I would need to conditionally put the rules in an or{}. I can't do something like

if(groupOp == 'or'){
    or{
}

before I go through the rulesList and then

if(groupOp == 'or'){
    }
}

afterward. All I can think to do is to repeat the code for each condition:

if(groupOp == 'or'){
    or{
        rulesList.each { rule ->
            switch(rule['op']){
                ...
            }
        }
    }
}
else{
    rulesList.each { rule ->
        switch(rule['op']){
            ...
        }
    }

Now the code is looking quite sloppy and repetitive. Suppose I want to search on a property of a property of the domain object? (Ex: I want to return vehicles whose tires are a certain brand; vehicle.tires.brand, or vehicles whose drivers match a name; vehicle.driver.name). Would I have to do something like:

switch(rule['op']){
    case 'eq':
        switch(thePropertiesProperty){
            case Garage:
                garage{
                    eq("${rule['field']}", rule['value'])
                }
                break
            case Driver:
                driver{
                     eq("${rule['field']}", rule['value'])
                }
                break
        }
        break
    case 'ne':
        ...
}
Epicardium answered 18/7, 2012 at 21:18 Comment(0)
C
10

First off, you can simplify your big switch by using a GString for the method name:

case ~/^(?:eq|ne|gt|ge|lt|le)$/:
  "${rule['op']}"("${rule['field']}", rule['value'])
  break

The same trick works for the and/or:

"${(groupOp == 'or') ? 'or' : 'and'}"() {
  rulesList.each { rule ->
    switch(rule['op']){
        ...
    }
  }
}

or you could assign the closure to a variable first and then call either or(theClosure) or and(theClosure) as appropriate. Finally, for the "property of a property" search, if you add

createAlias('driver', 'drv')
createAlias('garage', 'grg')

to the top of the criteria closure then you can query on things like eq('drv.name', 'Fred') without having to add the intervening driver {...} or garage {...} node.

Chinoiserie answered 18/7, 2012 at 22:0 Comment(2)
Great solution, it seems I am still not realizing and leveraging the full power of Groovy. Also, for anyone interested in learning more about createAlias (which I knew nothing about previously) look over hereEpicardium
Thanks! createAlias solved my problem. I have no idea why it is not mentioned on the createCriteria page of the Grails documentation.Medicine

© 2022 - 2024 — McMap. All rights reserved.