Mike Croteau
Mike Croteau

Reputation: 1132

Grails : how to best construct a hibernate criteria builder to search 'hasMany' relationships with domain instance

I am working on a grails project and would like to leverage hibernate criteria builders to search for instances of a domain object. I would like to find instances where one of the 'hasMany' relationships contains domain object with certain ids. Here is an example of what I mean.

Domain Objects

class Product {
   static hasMany = [ productOptions: ProductOption ]
}

class ProductOption{
   Option option
   static belongsTo = [ product: Product ]
}

class Option{
   String name
}

This is a simplified example of my domain structure and doesn't include all relationships.

An Option could be size, color, brand, etc.

Example of what I would like to achieve

Lets say I have 3 products.

Product 1 is red, small and by brandx

Product 2 is blue, small and by brandx

Product 3 is yellow, medium and by brandz

I have a few scenarios that I need to cover.

Scenario 1

Scenario 2

Scenario 3

I hope this covers all scenarios.

This is an example of a current attempt.

def c = Product.createCriteria()
def products = c.list{
    and {
        productOptions {
            'option' {
                idEq(1)//1 is the id of the blue option
            }
        }
        productOptions {
            'option' {
                idEq(5)//5 is the id of the small size option
            }
        }
        productOptions {
            'option' {
                idEq(10)//10 is the id of the brandx brand option
            }
        }
    }
}

The and portion of this example doesn't include all options and fails. How do I best achieve this? Can I use Grails hibernate criteria builder to achieve this? Please let me know if additional information will help.

Thanks in advance for any guidance provided.

Upvotes: 0

Views: 653

Answers (1)

Emmanuel Rosa
Emmanuel Rosa

Reputation: 9895

What you're looking for is the equivalent of Groovy's Object.every(Closure).

assert [1, 2, 3].every { it < 4 } == true assert [1, 2, 3].every { it < 3 } == false

The every() method returns a Boolean indicating whether the Closure evaluates to true for every item in the collection.

Unfortunately, none of the query methods (where, criteria, and HQL) provide an equivalent of every(). But... you can cheat using HQL.

Note: Where nor Criteria queries will do because they don't support the equivalent of the HQL HAVING clause.

Scenario #1 - The Hack

def ids = [4, 5, 6] // List of Option ids.

Product.executeQuery '''
select prd from Product as prd 
    join prd.productOptions as prdopts 
    join prdopts.option as opt 
where opt.id in :ids 
group by prd
having count(prd) = :count''', [ids: ids.collect { it.toLong() }, count: ids.size().toLong()]

How it works

The query begins by selecting all of the Products which have any of the Options in the ids list. As long as a Product has at least one of the options it will be returned.

This produces the side-effect of listing a Product for every matching option it has. For instance, if a Product has three of the Options, then the Product is returned three times. The GROUP BY clause makes the query filter out those duplicate listings.

However, those duplicates are key to this hack: if the list of IDs is a unique list, and Products do not have the same Option more than once, then the Product has all of the required Options if the number of duplicates is equal to the number of IDs. And that's what the HAVING clause does by counting the number of Products.

Scenario 2 & 3

Scenarios 2 & 3 can be handled by the same query. I'm going to forgo consistency and chose a Criteria query because it serves this purpose best.

// Example params for scenario 2
def qparams = [
    or: [1, 2], // These are color Option IDs
    and: 5 // This is a size Option ID
]

// Example params for scenario 3
def qparams = [
    or: [10, 11] // These are brand Option IDs
]

Product.withCriteria {
    productOptions {
        option {
            if(qparams.and) eq('id', qparams.and.toLong())
            inList('id', qparams.or.collect({ it.toLong() }))               
        }
    }
}

The or parameter is always expected, but the if block only adds the and constraint if the and parameter is specified. Notice that the IDs are all just Option IDs, so you have some flexibility. For instance, you can search for any colors without a size constraint.

About the IDs...

You'll notice that in my examples I converted the IDS from Integers to Longs. If you IDs are coming from the database, then they're already Longs so you can take that code out.

Upvotes: 2

Related Questions