shenku
shenku

Reputation: 12430

Restrict QueryOver by child collection using nHibernate

I am trying to get a parent entity where all entities in a child collection are in another list.

For example:

public class Parent {

public virtual int Id {get;set;}
public virtual List<Child> Children {get;set;}

}

public class Child {

public virtual int Id {get;set;}
public virtual string Name {get;set;}

}

I've tried various combinations of Joins and Restrictions but can't seem to hit the spot.

So please help with suggestions.

Current example below:

 public IList<Lead> GetAllAvailable(string[] names)
    { 
        var result =  Session.CreateCriteria<Parent>()
            .CreateCriteria("Children")
            .Add(Expression.In("Name", names)).List<Parent>();

        return result;
    }

Edit:

This is the sql equivilent:

select  *
from    dbo.Parent
        join ( select   p.id
               from     dbo.Parent p
                        join dbo.ParentToChildren on p.Id = dbo.ParentsToChildren.Parent_Id
                        join dbo.Child on dbo.ParentToChildren.Child_Id = dbo.Child.Id
               where    Name in ( 'foo', 'bar' )
               group by p.Id
               having   count(1) > 1
             ) as foo on dbo.Parent.Id = foo.Id

Upvotes: 4

Views: 4776

Answers (1)

Andre Loker
Andre Loker

Reputation: 8408

Here's my suggestion:

var parents = session.QueryOver<Child>()
  .WhereRestrictionOn(x => x.Name).IsIn(names)
  .Select(Projections.Group<Child>(x => x.Parent))
  .Where(Restrictions.Ge(Projections.Count<Child>(x => x.Parent), names.Length))
  .List<Parent>();

The idea is as follows: find all children that have a Name like one of the names entries. Group those children by their Parent. Your Child will need a Parent property mapped to the respective parent for this, but that's a good idea anyway. For all groups with a size equal to (or greater than, but that shouldn't happen, so you could replace Ge with Eq) names.Length, return their parent; because if the size of the group is equal to names.Length, all names have been found assuming no two children of a parent have the same name.

The generated query:

SELECT
    this_.Parent as y0_
FROM
    Child this_
WHERE
    this_.Name in (
        /*  */
    )
GROUP BY
    this_.Parent
HAVING
    count(this_.Parent) >= /* names.Length */;

I've created a test app that returned promising results.

If you need to do more with the parents, like paging or fetching the children, you could split this problem into a sub query (note that the .Fetch(x=>x.Children).Eager line is not required, it's just an example what you can further do with the query):

var parentSubQuery =
  QueryOver.Of<Child>()
    .WhereRestrictionOn(x => x.Name).IsIn(names)
    .Select(Projections.Group<Child>(x => x.Parent))
    .Where(Restrictions.Ge(Projections.Count<Child>(x => x.Parent), names.Length));

var parents = session.QueryOver<Parent>() 
  .Fetch(x=>x.Children).Eager // not necessary, just an example
  .WithSubquery.WhereProperty(x => x.Id).In(parentSubQuery )
  .List();

SQL (without the Fetch):

SELECT
    this_.Id as Id1_0_
FROM
    Parent this_
WHERE
    this_.Id in (
        SELECT
            this_0_.Parent as y0_
        FROM
            Child this_0_
        WHERE
            this_0_.Name in (
                /* names */
            )
        GROUP BY
            this_0_.Parent
        HAVING
            count(this_0_.Parent) >= /* names.length */
    );

Update:

If Parent<->Child is many-to-many, things get a little bit trickier:

      Parent parent = null;
      var parentSubQuery = QueryOver.Of<Child>()
        .WhereRestrictionOn(x => x.Name).IsIn(names)
        .JoinQueryOver(x => x.Parents, () => parent)
        .Where(Restrictions.Ge(Projections.Count(() => parent.Id), names.Length))
        .Select(Projections.Group(() => parent.Id));

      var parents = session.QueryOver<Parent>()
        .WithSubquery.WhereProperty(x => x.Id).In(parentSubQuery)
        .List();

The main difference is that instead of grouping by the direct Parent property of Child I first needed to join the parents collection. To reference each parent there I introduce an alias parent.

The generated SQL is pretty close to the original approach:

SELECT
    this_.Id as Id2_0_
FROM
    Parent this_
WHERE
    this_.Id in (
        SELECT
            parent1_.Id as y0_
        FROM
            Child this_0_
        inner join
            ChildToParent parents3_
                on this_0_.Id=parents3_.ChildId
        inner join
            Parent parent1_
                on parents3_.ParentId=parent1_.Id
        WHERE
            this_0_.Name in (
                /* names */
            )
        GROUP BY
            parent1_.Id
        HAVING
            count(parent1_.Id) >= /* names.Length */
    );

For my test scenario it works, so hopefully it will for you, too.

Upvotes: 3

Related Questions