Search code examples
nhibernatenhibernate-criteria

Reversing the logic of NHibernate query


I have constructed the following query using NHibernate which will give me a collection of MenuView items which contain a given page (referenced by the pages id).

// Only retrieve the required properties from Menu object
ProjectionList menuViewProjections = Projections.ProjectionList()
    .Add(Projections.Property("ID"), "ID")
    .Add(Projections.Property("Name"), "Name")
    .Add(Projections.Property("Description"), "Description");

var menus = session.CreateCriteria(typeof(Menu))
    // Only menu's that are editable
    .Add(Restrictions.Eq("IsEditable", true))

    // Only project required properties
    .SetProjection(menuViewProjections)

    // Only menu's that contain this page (Menu object has IList<Page> property called 'Pages')
    .CreateCriteria("Pages")
    // Restrict to menu's containing the pages with an id of the specified value
    .Add(Restrictions.Eq("ID", pageId))

    // Transform results into required, light-weight, view objects
    .SetResultTransformer(Transformers.AliasToBean(typeof(MenuView)))
    .List<MenuView>();

This works fine; however, now I want to do the opposite: I want to query for all editable menu objects that do not contain the page with specified ID. I have thus far not found a solution for this. I would have thought a simple reversal of the pages section of the above query would suffice resulting in:

// Only retrieve the required properties from Menu object
ProjectionList menuViewProjections = Projections.ProjectionList()
    .Add(Projections.Property("ID"), "ID")
    .Add(Projections.Property("Name"), "Name")
    .Add(Projections.Property("Description"), "Description");

var menus = session.CreateCriteria(typeof(Menu))
    // Only menu's that are editable
    .Add(Restrictions.Eq("IsEditable", true))

    // Only project required properties
    .SetProjection(menuViewProjections)

    // Only retrieve menus that do NOT contain this referenced page
    .CreateCriteria("Pages")
    .Add(Restrictions.Not(Restrictions.Eq("ID", pageId)))

    // Transform results into required view objects
    .SetResultTransformer(Transformers.AliasToBean(typeof(MenuView)))
    .List<MenuView>();

But, this results in the following SQL:

SELECT this_.ID          as y0_,
   this_.Name        as y1_,
   this_.Description as y2_
FROM   [Menu] this_
       inner join PagesInMenu pages3_
         on this_.ID = pages3_.MenuID
       inner join [Page] page1_
         on pages3_.PageID = page1_.ID
WHERE  this_.IsEditable = 1 /* @p0 */
       and not (page1_.ID = 8 /* @p1 */)

Which is still returning results of Menu items that do contain a page with id of 8. Why is this simple reversal of logic not so simple in terms of code?

[Update] Taking on the suggestions from Firo, the suggested query alteration to;

// Only retrieve menus that do NOT contain this referenced page
.CreateCriteria("Pages")
.Add(Subqueries.PropertyNotIn("Id", querymenuItemswithPage))   <--- query you have would be here

Now generates the following sql statement;

    SELECT this_.ID          as y0_,
       this_.Name        as y1_,
       this_.Description as y2_
FROM   [Menu] this_
       inner join PagesInMenu pages3_
         on this_.ID = pages3_.MenuID
       inner join [Page] page1_
         on pages3_.PageID = page1_.ID
WHERE  this_.IsEditable = 1 /* @p0 */
       and page1_.ID not in (SELECT this_0_.ID as y0_
                             FROM   [Page] this_0_
                             WHERE  this_0_.ID = 1 /* @p1 */

)

Which at first seems exactly what I wanted but sadly (probably due to my poor understanding of joins) is still not returning quite what I wanted. Given the following tables

Menu

Shot of menu table

And then the join-table of PagesInMenu (with a WHERE clause of WHERE PageID = 1)

Show of join table

We can see that page with id of 1 is NOT referenced in menus 5 and 6. I expect the query in question to only return a single row, which would be the ID, Name and Description of Menu with ID of 5 as this is the only menu which page 1 is not included in and which is editable

Instead, the new query returns;

enter image description here

I have crossed out all the rows that are returned but shouldnt be. What is going on here !?


Solution

  • Update:

    • removed .CreateCriteria("Pages")
    • added subquery

      var querymenuItemswithPage = DetachedCriteria.For<Menu>()
      .CreateCriteria("Pages")
          .Add(Restrictions.Eq("ID", pageId))
      .SetProjection(Projections.Id())
      
      // Only retrieve the required properties from Menu object
      ProjectionList menuViewProjections = Projections.ProjectionList()
          .Add(Projections.Property("ID"), "ID")
          .Add(Projections.Property("Name"), "Name")
          .Add(Projections.Property("Description"), "Description");
      
      var menus = session.CreateCriteria(typeof(Menu))
          // Only menu's that are editable
          .Add(Restrictions.Eq("IsEditable", true))
      
          // Only project required properties
          .SetProjection(menuViewProjections)
      
          // Only retrieve menus that do NOT contain this referenced page
          .Add(Subqueries.PropertyNotIn("Id", querymenuItemswithPage))
      
          // Transform results into required view objects
          .SetResultTransformer(Transformers.AliasToBean(typeof(MenuView)))
          .List<MenuView>();