Fork me on GitHub
Showing posts with label criteria queries. Show all posts
Showing posts with label criteria queries. Show all posts

13 Jun 2009

Querying by Association Redux

A couple of months back I posted about the problems with criteria queries where you want results with a collection property that contains some value.

Last night I was reading through my new copy of Glen Smith and Peter Ledbrook's Grails In Action and my eye was caught by a particular example where they use aliases in a criteria query. Sure enough a quick test this morning confirms that this is the solution to the problem.

To recap. If I have these domain classes:

class Pirate {
String name
static belongsTo = Ship
static hasMany = [ships: Ship]
}

class Ship {
String name
static hasMany = [crew: Pirate]
}

and I want to query for all the Ships containing a particular Pirate I would probably try to do this:

def ships = Ship.withCriteria {
crew {
eq("name", "Blackbeard")
}
}

Unfortunately, while I end up with the correct Ship instances, each of their crew collections only contains the item(s) that matched the criteria regardless of any others that may actually exist.

Using an alias we can do this type of query without resorting to HQL:

def ships = Ship.withCriteria {
createAlias("crew", "c")
eq("c.name", "Blackbeard")
}

A complete test case that proves the point using the domain classes above is:

class PirateTests extends GroovyTestCase {

void setUp() {
Ship.withSession {session ->
def blackbeard = new Pirate(name: "Blackbeard")
def jack = new Pirate(name: "Calico Jack")
def bart = new Pirate(name: "Black Bart")
[blackbeard, jack, bart]*.save()

def ship1 = new Ship(name: "Queen Anne's Revenge")
ship1.addToCrew blackbeard
ship1.addToCrew jack

def ship2 = new Ship(name: "Royal Fortune")
ship2.addToCrew blackbeard
ship2.addToCrew bart

def ship3 = new Ship(name: "The Treasure")
ship3.addToCrew jack
ship3.addToCrew bart

[ship1, ship2, ship3]*.save()

session.flush()
session.clear()
}
}

void testQueryByAssoc() {
def ships = Ship.withCriteria {
crew {
eq("name", "Blackbeard")
}
order("name", "asc")
}

assertEquals 2, ships.size()
assertEquals "Queen Anne's Revenge, Royal Fortune", ships.name.join(", ")
assertEquals "Expected 2 crew but found ${ships[0].crew.name}", 2, ships[0].crew.size()
assertEquals "Expected 2 crew but found ${ships[1].crew.name}", 2, ships[1].crew.size()
}

void testQueryByAssocUsingAlias() {
def ships = Ship.withCriteria {
createAlias("crew", "c")
eq("c.name", "Blackbeard")
order("name", "asc")
}

assertEquals 2, ships.size()
assertEquals "Queen Anne's Revenge, Royal Fortune", ships.name.join(", ")
assertEquals "Expected 2 crew but found ${ships[0].crew.name}", 2, ships[0].crew.size()
assertEquals "Expected 2 crew but found ${ships[1].crew.name}", 2, ships[1].crew.size()
}
}

The first test fails as each Ship returned by the query only has Blackbeard in the crew despite the fact that they were created and saved with 2 crew members each. The second test retrieves the correct results.

Logging out the SQL generated by the Hibernate queries shows the difference in what it's doing under the hood. The first (incorrect) query is:

select * from ship
left outer join ship_crew on ship.id = ship_crew.ship_id
left outer join pirate on ship_crew.pirate_id = pirate.id
where pirate.name = ?
order by ship.name asc

The second (correct) query is:

select * from ship
inner join ship_crew on ship.id = ship_crew.ship_id
inner join pirate on ship_crew.pirate_id = pirate.id
where pirate.name = ?
order by ship.name asc

So the first query is using left outer joins and the second is using inner joins. Running those queries directly in SQuirreL returns basically the same result set so the discrepancy must be in how Hibernate treats the results. It seems Hibernate has populated the Ship instance and its crew collection after the first query and therefore considers the collection to be initialized, although actually the data in the result set was not the complete collection. In contrast, after the second query you can see the individual SQL select statements as the lazy-loading fires when the assertions are done, so Hibernate obviously only populated the root Ship instance from the query results and is treating the collection property as uninitialized.

1 Apr 2009

Querying By Association With Grails Criteria

A common requirement is to select all instances of a domain class where one of its many-to-many associations contains a particular instance of another domain class. Consider these domain classes where a Ship has a crew composed of Pirates and any particular Pirate can be a part of the crew of several Ships:

class Ship {
static hasMany = [crew: CrewMember]
String name
}

class CrewMember {
static belongsTo = [ship: Ship]
Pirate pirate
}

class Pirate {
String name
}

How can we use a criteria query to find all the Ship instances where a particular Pirate is a member of the crew? You might think it's pretty simple, surely:

Ship.withCriteria {
crew {
eq('pirate', blackbeard)
}
}

However, this has a curious side-effect. For example:

Ship.withSession {session ->
blackbeard = new Pirate(name: 'Blackbeard')
jack = new Pirate(name: 'Calico Jack')
bart = new Pirate(name: 'Black Bart')
[blackbeard, jack, bart]*.save()

def ship1 = new Ship(name: "Queen Anne's Revenge")
ship1.addToCrew new CrewMember(pirate: blackbeard)
ship1.addToCrew new CrewMember(pirate: jack)

def ship2 = new Ship(name: "Royal Fortune")
ship2.addToCrew new CrewMember(pirate: blackbeard)
ship2.addToCrew new CrewMember(pirate: bart)

def ship3 = new Ship(name: "The Treasure")
ship3.addToCrew new CrewMember(pirate: jack)
ship3.addToCrew new CrewMember(pirate: bart)

[ship1, ship2, ship3]*.save()

session.flush()
session.clear()
}

def blackbeardsShips = Ship.withCriteria {
crew {
eq('pirate', blackbeard)
}
}
blackbeardsShips.each {
println "$it.name: ${it.crew.pirate.name.join(', ')}"
}

You might expect the output to be:

Queen Anne's Revenge: Blackbeard, Calico Jack
Royal Fortune: Blackbeard, Black Bart

but it is actually:

Queen Anne's Revenge: Blackbeard
Royal Fortune: Blackbeard

The criteria query has restricted the content of the associations to only the ones matching the eq('pirate', blackbeard) criterion. This is quite a problem as it may well not be immediately obvious that it's happened and even using ships*.refresh() (which would be a horrible hack, especially if we were likely to get more than a couple of results) doesn't seem to restore the missing entries. I don't think there is any criterion that does a 'contains' type match for a persistent collection. My guess is this problem isn't a Grails thing but a Hibernate thing - Grails' HibernateCriteriaBuilder is a very thin layer over Hibernate itself.

In this example there is an alternative as the association is bi-directional. We can query from the other side of the association:

def blackbeardsShips = CrewMember.withCriteria {
projections {
property('ship')
}
eq('pirate', blackbeard)
}

If the association wasn't bidirectional this wouldn't be possible and we'd probably have to resort to using HQL to make the query exhibit the correct behaviour.