TL;DR version: Hibernate's Criteria API is deprecated. It has some quirks. Deal with it or migrate your code! (You should probably migrate sometime soon by the way). More specifically, if you require JOINs between multiple layers of collections, be sure to either use INNER_JOIN (vs. LEFT_OUTER_JOIN) or have a useless restriction on your collection (such as Restrictions.isNotEmpty( collection )), lest you find yourself with a strangely initialized collection.
Long version:
Once upon a time, a big figure from Hibernate declared that people would never be able to EAGER-initialize a collection when they add a criteria on it. That is, when finding an object, if your algorithm requires a certain condition on a collection within that object, that collection would have to be LAZY-initialized.
Well, that would be OK if it wasn't for a certain curious behavior.
Basically, specifying query restrictions on sub-criterias of collections could result on an object with a filtered collection.
Whoosh! Ok, let me try again. Let there be Cat:
Entity Cat has:
- Name (String)
- Mate (ManyToMany(let's simplify...) OneToOne Cat)
- Parents (ManyToMany Set<Cat>)
- Kittens (ManyToMany Set<Cat>)
- LikesFood (ManyToMany Set<Food>)
Now let there be some cats:
Myrthe ----- John Gretta ----- Charles
| |
---------- -----------
| | | |
Sam Phil --- Kitty Mr. Furbal
And some types of food:
- Royal Canin
- Iams
- Bubba Gump
Now it just happens that Phil, Kitty and Mr. Furbal all like Royal Canin, while Sam, Phil and Kitty like Iams and Sam, Kitty and Mr. Furbal like Bubba Gump.
Our fictious Pet Shop seems to have customers that will choose cats based on which food their siblings like, so we come up with something like this to feed to our CatDAO (pun intended):
session.createCriteria( Cat.class ).createCriteria( "Kittens", JoinType.LEFT_OUTER_JOIN ).createCriteria( "LikesFood", JoinType.LEFT_OUTER_JOIN ).add( Restrictions.eq( "foodName", foodName ) );
That is, if a customer comes in and says "Hoy! I want the entire litter where any one kitten likes Royal Canin", we could now find that out by running that query and looking at the Kittens collection of the resulting cats.
It's probably an horrible example, but the point is: it won't work. And it won't work because both Myrthe and John will only have Phil in their "Kittens" collection. Sam got filtered out because it doesn't like Royal Canin.
This curious behavior happens because the Criteria API seems to conclude that the LEFT_OUTER_JOIN on the collection allows it to be initialized with the query results... UNLESS there's a restriction on that JOIN.
Now there are two types of people. People that expects objects returned by Hibernate to be 100% coherent to what's in the database (me included), and people wanting to have a feature to filter the contents of such objects (search and you'll find them).
The bad news is that LEFT_OUTER_JOIN will do the latter. The good news is that it's still possible to have the former behavior by working around this "feature":
- Use INNER_JOIN wherever possible instead of LEFT_OUTER_JOIN if joining on a collection.
- If LEFT_OUTER_JOIN must be used on collection, be sure to have a (dummy) restriction on the collection, otherwise it may get loaded with inconsistent elements. A restriction blocks the collection from being initialized with the results of the sub query. The collection will then be loaded as expected when used by the code (lazy initialization).
- Example of "dummy" restriction: Restrictions.isNotEmpty( collection ). Rationale: if you're left-joining on it to get to a child element and filter on it, the matched collection can't be empty anyway.
The behavior is so crazy that if instead of a Set you have a List and depending on what you're joining, you can even find DUPLICATES in your collection, which is REALLY not coherent against the database.
If you want an official statement, or more details on the issue, check here.