Search code examples
c#nhibernatenhibernate-criteria

NHibernate Filtered Child Collection Lazy Loaded even with eager fetch specified


Im trying to find out why a child collection is being returned without filtering even when eager loading the collection and the generated SQL is correct.

The fluent mappings for the classes are:

public class OptionIdentifierMap : ClassMap<OptionIdentifier>
{
    public OptionIdentifierMap()
        : base("OptionIdentifier")
    {
        //Id Mapping Removed
        HasMany<OptionPrice>(x => x.OptionPrices)
             .KeyColumn("OptionIdentifier_id")
             .Cascade.None();
    }
}

public class OptionPriceMap : ClassMap<OptionPrice>
{
    public OptionPriceMap()
        : base("OptionPrice")
    {
        //Id Mapping removed
        References(x => x.Option)
             .Column("OptionIdentifier_id")
             .Cascade.None()
             .ForeignKey("FK_OptionPrice_OptionIdentifier_id_OptionIdentifier_Id")
             .Not.Nullable();
        References(x => x.Increment)
             .Column("PricingIncrement_id")
             .Cascade.None()
             .ForeignKey("FK_OptionPrice_PricingIncrement_id_PricingIncrement_Id")
             .Not.Nullable();
        Map(x => x.Price).Not.Nullable();
    }
}

and PricingIncrement mapping

public class PricingIncrementMap : ClassMap<PricingIncrement>
{
    public PricingIncrementMap()
        : base("PricingIncrement")
    {
        Map(x => x.IncrementYear);
        HasMany<OptionPrice>(x => x.Options)
             .KeyColumn("PricingIncrement_id")
             .Cascade.None().Inverse();
    }
}

And the Entities are:

public class PricingIncrement : Entity
{
    public PricingIncrement()
    {
        Options = new List<OptionPrice>();
    }

    public virtual int IncrementYear { get; set; }
    public virtual IList<OptionPrice> Options { get; set; }
}

public class OptionPrice : Entity
{
    public OptionPrice()
    {
    }

    public virtual OptionIdentifier Option { get; set; }
    public virtual PricingIncrement Increment { get; set; }
    public virtual float Price { get; set; }
}

public class OptionIdentifier : Entity
{
    public OptionIdentifier()
    {
        OptionPrices = new List<OptionPrice>();
    }
            public virtual IList<OptionPrice> OptionPrices { get; set; }
}

Im trying to query All the OptionIdentifier that have an optionprice value for an specific PricingIncrement. The SQL Query that nhibernate generates from my criteria is:

SELECT this_.Id                      as Id37_4_,
   .......
FROM   OptionIdentifier this_       inner join OptionPrice op2_         on this_.Id = op2_.OptionIdentifier_id
   inner join PricingIncrement i3_         on op2_.PricingIncrement_id = i3_.Id
WHERE  (this_.IsDeleted = 0)
   and this_.Id in (7)
   and i3_.IncrementYear = 2015

The criteria I'm using to build this query is:

ICriteria pagedCriteria = this.Session.CreateCriteria<OptionIdentifier>()
                .CreateAlias("OptionPrices", "op", JoinType.InnerJoin)
                .CreateAlias("op.Increment", "i", JoinType.InnerJoin)
                .SetFetchMode("op", FetchMode.Eager)
                .SetFetchMode("i", FetchMode.Eager)
                .Add(Restrictions.Eq("i.IncrementYear", 2015))
                .Add(Expression.In("Id", idList.ToList<int>()))
                .SetResultTransformer(CriteriaSpecification.DistinctRootEntity);

When looking at SQL Profiler, the query executes and the result is correct, i get one row for each child in the OptionPrice table that matches the criteria, in my case one, from the available 4 rows that match the OptionIdentifier (there are 4 rows in PricingIncrement and 4 in OptionPrice one for each PricingIncrement for the OptionIdentifier_id 7)

But when i try to iterate the collection to get some values, for some reason nhibernate is loading the child collection, as if lazy load was specified, and loading the full 4 child rows. Reading the documentation FetchMode is supposed to fix this preventing nhibernate to lazy load child collections. Similar to a N+1 common issue.

I checked the SQL Profiler to see whats happening and nhibernate is generating queries without the original filter to fill the child collection when i try to access it. If i dont access the collection no query is generated.

Doing some testing i tried different join types and fetch modes, and so far the only way to iterate the collection without having hibernate load all the elements is to specify in the join type LeftOuterJoin, but this means something different.

I tried to search for issues similar but all of them say that eager loading should work, or mention that i should use filters. And so far i havent found any answer.

Any help is greatly appreciated.


Solution

  • I would like to share my approach, maybe not the answer...

    I. avoid fetching one-to-many (collections)

    When creating any kind of complex queries (ICriteria, QueryOver) we should use (LEFT) JOIN only on a start schema. I.e. on many-to-one (References() in fluent). That leads to expected row count from the perspective of paging (there is always only ONE row per root Entity)

    To avoid 1 + N issue with collections (but even with many-to-one in fact) we have the NHiberante powerful feature:

    19.1.5. Using batch fetching

    NHibernate can make efficient use of batch fetching, that is, NHibernate can load several uninitialized proxies if one proxy is accessed (or collections. Batch fetching is an optimization of the lazy select fetching strategy)...

    Read more here:

    So, in our case, we would adjust mapping like this:

    public PricingIncrementMap()
        : base("PricingIncrement")
    {
        Map(x => x.IncrementYear);
        HasMany<OptionPrice>(x => x.OptionPrices)
            .KeyColumn("OptionIdentifier_id")
            .Cascade.None()
            .Inverse() // I would use .Inverse() as well
            // batch fetching
            .BatchSize(100);
    }
    

    II. collection filtering

    So, we managed to avoid 1 + N issue, and we also query only star schema. Now, how can we load just filtered set of items of our collection? Well, we have native and again very powerful NHibernate feature:

    18.1. NHibernate filters.

    NHibernate adds the ability to pre-define filter criteria and attach those filters at both a class and a collection level. A filter criteria is the ability to define a restriction clause very similiar to the existing "where" attribute available on the class and various collection elements...

    Read more about it here:

    So in our case we would define filter

    public class CollFilter : FilterDefinition
    {
        public CollFilter()
        {
            WithName("CollFilter")
                .WithCondition("PricingIncrement_id = :pricingIncrementId")
                .AddParameter("pricingIncrementId",NHibernate.Int32);
        }
    } 
    

    And we would need to extend our mapping again:

    HasMany<OptionPrice>(x => x.OptionPrices)
        .KeyColumn("OptionIdentifier_id")
        .Cascade.None()
        .Inverse()
        // batch fetching
        .BatchSize(100)
        // this filter could be turned on later
        .ApplyFilter<CollFilter>();
    

    Now, before our query will be executed, we just have to enable that filter and provide proper ID of the year 2015:

    // the ID of the PricingIncrement with year 2015
    var pricingIncrementId thes.Session
         .QueryOver<PricingIncrement>()
         .Where(x => x.IncrementYear == 2015)
         .Take(1)
         .Select(x => x.ID)
         .SingleOrDefault<int?>();
    
    this.Session
       .EnableFilter("CollFilter")
       .SetParameter("pricingIncrementId", pricingIncrementId);
    
    // ... the star schema query could be executed here
    

    III. Sub-query to filter root entity

    Finally we can use sub-query, to restrict the amount of root entities to be returned with our query.

    15.8. Detached queries and subqueries

    Read more about it here:

    so, our subquery could be

    // Subquery
    var subquery = DetachedCriteria.For<OptionPrice >()
        .CreateAlias("Increment", "i", JoinType.InnerJoin)
        .Add(Restrictions.Eq("i.IncrementYear", 2015))
        .SetProjection(Projections.Property("Option.ID"));
    
    // root query, ready for paging, and still filtered as wanted
    ICriteria pagedCriteria = this.Session.CreateCriteria<OptionIdentifier>()
        .Add(Subqueries.PropertyIn("ID", subquery))
        .SetResultTransformer(CriteriaSpecification.DistinctRootEntity);
    

    Summary: We can use lot of features, which are shipped with NHibernate. They are there for a reason. And with them together we can achieve stable and solid code, which is ready for further extending (paging at the first place)

    NOTE: maybe I made some typos... but the overall idea should be clear