I have a table called Recipes
which contain one recipe per row. I also have a table called RecipeIngredients
which contain one ingredient as used by a particular recipe. Thus, each Recipe
row has one or more children RecipeIngredients
rows.
What I'm trying to do is create a query to find all recipes that contain any ingredients in a list of desired ingredients. For example, show me all recipes that use either flour, eggs, or bananas.
The SQL would look something like this:
SELECT * FROM Recipes r
WHERE EXISTS (select 1 from RecipeIngredients where RecipeId = r.RecipeId and IngredientId = ANY (5, 10, 15) limit 1);
However, I'm having a tough time figuring out how to express this as a LINQ query, or using the .QueryOver<T>
method. I don't want to hard code in the SQL since this needs to be database agnostic and I want the configured NHibernate dialect to generate the correct code.
Any ideas?
NHibernate has support for this SQL statements, called
- 15.8. Detached queries and subqueries,
- 16.8. Subqueries
The syntax would be like this:
var session = ...// get a ISession
Reciepe reciepe = null; // this will be a reference to parent
// the SELECT inside of EXISTS
var subquery = QueryOver.Of<ReciepeIngredient>()
// The PARENT handling here
// the filter, to find only related ingredients
.Where(item => item.ReciepeId == reciepe.ID)
.Where(Restrictions.In("ID", new[] { 5, 10, 15 }))
// Select clause
.Select(ing => ing.ID)
;
Having the above subquery, we can use it like this
// the '() => reciepe' setting is essential here, it represents parent in a subquery
var query = session.QueryOver<Reciepe>(() => reciepe);
query.WithSubquery
// our EXISTS (...
.WhereExists(subquery);
var list = query
.List<Reciepe>();
NOTE: let's check even more deeper subquery(ies) usage here Query on HasMany reference
A Few More Details:
Radim's answer turns out to be the best way to express the sub-query, however there's a few gotchas that took me a while to figure out. Thus, I'll post an answer as well to fill in the details.
First off, the line:
.Where(Restrictions.In("ID", new[] { 5, 10, 15 }))
Doesn't actually work if ID
refers to an entity itself. In other words:
.Where(Restrictions.In("Ingredient", arrayOfIds))
Will throw a very confusing null reference exception since the Ingredient field maps to a Ingredients
object. Using "IngredientId"
doesn't work either. In that case, you have to use this:
.Where(Restrictions.In("Ingredient", arrayOfIds
.Select(id => new Ingredients(id)).ToArray()))
To cast the ID array to an array of Ingredients
objects. After that, things start working.
I also found an easy performance improvement that made the query run noticably faster, at least on PostgreSQL. If you change the sub-query from:
WHERE exists (SELECT RecipeIngredientId FROM recipeingredients WHERE
RecipeId = r.RecipeId and IngredientId in (:p0, :p1))
To:
WHERE exists (SELECT RecipeIngredientId FROM recipeingredients WHERE
RecipeId = r.RecipeId and IngredientId in (:p0, :p1) LIMIT 1)
It will only have to check a single row within the nested query. The query ran about twice as fast for me. This is easy to express:
var subquery = QueryOver.Of<RecipeIngredients>()
.Where(item => item.Recipe.RecipeId == recipe.RecipeId)
.Where(Restrictions.In("Ingredient", allowedIngs))
.Select(i => i.RecipeIngredientId).Take(1);
Hope this helps!
Try this Linq query:
recipes.Where(r => r.RecipeIngredients.Any(i => new long[]{5, 10, 15}.Contains(i.Id)));