0

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

  • Find products that are blue, small and by brandx. So in this case I should only return Product 2.

Scenario 2

  • Find products that are either red or blue and size small. So both Product 1 and Product 2 should be returned.

Scenario 3

  • Find products that are either by brandx or brandz. So all products should be returned.

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.

Mike Croteau
  • 1,062
  • 2
  • 16
  • 43

1 Answers1

2

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.

Community
  • 1
  • 1
Emmanuel Rosa
  • 9,697
  • 2
  • 14
  • 20
  • Thanks, this seems to be working better. I am currently testing, is there a way to modify the query for another scenario? Lets say I have the same 3 products with the same options, and I want to retrieve all products that are either red and blue and small size. Apologies for not specifying this case, but just discovered it when testing your query. Thanks again for your time and help. – Mike Croteau Sep 30 '15 at 20:02
  • That would have to be a separate query. Do you actually mean (_red or blue_) and small size? – Emmanuel Rosa Sep 30 '15 at 20:08
  • Yeah, I updated the question with a little more detail. To sum it up, if possible I would like to mimic the catalog filters you would find on a modern day ecommerce site. – Mike Croteau Sep 30 '15 at 20:12
  • Thanks, I think I may be able to cover all 3 scenarios at once running the first query for each option combination. I am currently working on this approach. – Mike Croteau Oct 01 '15 at 00:01