1

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?

Mike Christensen
  • 88,082
  • 50
  • 208
  • 326

3 Answers3

2

NHibernate has support for this SQL statements, called

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

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Excellent! I will mess around with this tomorrow and see if I can get it working.. Thanks! – Mike Christensen Jan 23 '14 at 05:43
  • Great! NHibernate is awesome tool, sir ;). What is more important? You've choosen to correct if not the best way. With subqueries you will have filtered root entity (reciepe) while it will still stay flat, not multiplied. Lot of people try to use fetching ... which ends in the cartesian products (in case of fetching collections). This approach (subqueries) is out of the box ready for paging (Take(), Skip()). Great... – Radim Köhler Jan 23 '14 at 05:48
  • Got it working! I added my own answer below as well with a few more details I discovered. However, your approach works great and generates fast and efficient SQL. – Mike Christensen Jan 23 '14 at 18:44
1

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!

Mike Christensen
  • 88,082
  • 50
  • 208
  • 326
0

Try this Linq query:

        recipes.Where(r => r.RecipeIngredients.Any(i => new long[]{5, 10, 15}.Contains(i.Id)));
  • Thanks! If I run this, I get: An unhandled exception of type 'System.Exception' occurred in NHibernate.dll Additional information: Unrecognised method call: System.Linq.Enumerable:Boolean Any[TSource](System.Collections.Generic.IEnumerable`1[TSource], System.Func`2[TSource,System.Boolean]) – Mike Christensen Jan 23 '14 at 16:41