NHibernate Eager Fetching Over Multiple Levels
Asked Answered
E

5

19

I have a 3-leveled hierarchy of entities: Customer-Order-Line, which I would like to retrieve in entirety for a given customer, using ISession.Get(id). I have the following XML fragments:

customer.hbm.xml:

<bag name="Orders" cascade="all-delete-orphan" inverse="false" fetch="join">
  <key column="CustomerID" />
  <one-to-many class="Order" />
</bag>

order.hbm.xml:

<bag name="Lines" cascade="all-delete-orphan" inverse="false" fetch="join">
  <key column="OrderID" />
  <one-to-many class="Line" />
</bag>

I have used the fetch="join" attribute to indicate that I want to fetch the child entities for each parent, and this has constructed the correct SQL:

SELECT 
    customer0_.ID AS ID8_2_, 
    customer0_.Name AS Name8_2_, 
    orders1_.CustomerID AS CustomerID__4_, 
    orders1_.ID AS ID4_, 
    orders1_.ID AS ID9_0_, 
    orders1_.PostalAddress AS PostalAd2_9_0_, 
    orders1_.OrderDate AS OrderDate9_0_, 
    lines2_.OrderID AS OrderID__5_, 
    lines2_.ID AS ID5_, 
    lines2_.ID AS ID10_1_, 
    lines2_.[LineNo] AS column2_10_1_, 
    lines2_.Quantity AS Quantity10_1_, 
    lines2_.ProductID AS ProductID10_1_ 

FROM Customer customer0_ 

LEFT JOIN [Order] orders1_ 
       ON customer0_.ID=orders1_.CustomerID 

LEFT JOIN Line lines2_ 
       ON orders1_.ID=lines2_.OrderID 

WHERE customer0_.ID=1

So far, this looks good - SQL returns the correct set of records (with only one distinct orderid), but when I run a test to confirm the correct number of entities (from NH) for Orders and Lines, I get the wrong results

I should be getting (from my test data), 1xOrder and 4xLine, however, I am getting 4xOrder and 4xLine. It appears that NH is not recognising the 'repeating' group of Order information in the result set, nor correctly 'reusing' the Order entity.

I am using all integer IDs (PKs), and I've tried implementing IComparable of T and IEquatable of T using this ID, in the hope that NH will see the equality of these entities. I've also tried overridding Equals and GetHashCode to use the ID. Neither of these 'attempts' have succeeded.

Is "multiple leveled fetch" a supported operation for NH, and if so, is there an XML setting required (or some other mechanism) to support it?


NB: I used sirocco's solution with a few changes to my own code to finally solve this one. the xml needs to be changed from bag to set, for all collections, and the entitities themselves were changed to implement IComparable<>, which is a requirement of a set for uniqueness to be established.

public class BaseEntity : IComparable<BaseEntity>
{
    ...

    private Guid _internalID { get; set; }
    public virtual Guid ID { get; set; }

    public BaseEntity()
    {
        _internalID = Guid.NewGuid();
    }

    #region IComparable<BaseEntity> Members

    public int CompareTo( BaseEntity other )
    {
        if ( ID == Guid.Empty || other.ID == Guid.Empty )
            return _internalID.CompareTo( other._internalID );

        return ID.CompareTo( other.ID );
    }

    #endregion

    ...

 }

Note the use of an InternalID field. This is required for new (transient) entities, other wise they won't have an ID initially (my model has them supplied when saved).

Ethbun answered 2/12, 2008 at 0:44 Comment(1)
This answer helped me see how to use QueryOver and Future queries to eagerly fetch children and grandchildren without returning duplicates. The technique involves breaking the task down into separate SQL queries that are executed in one roundtrip to the database.Ductile
A
21

You're getting 4XOrder and 4XLines because the join with lines doubles the results . You can set a Transformer on the ICriteria like :

.SetResultTransformer(new DistinctRootEntityResultTransformer())
Amr answered 15/12, 2008 at 12:45 Comment(4)
I discovered that this does actually solve the problem, if the mappings are changed from bag to set, and I implement the necessary IComparable<T> on the base class.Ethbun
Interesting, I have used the DistinctRoot .... but always with a Bag, and never implementing IComparable. But never did go for 3 level loading :)Amr
Also, I think it would be best not to Load the customer and then go Customer.Orders. In general you don't ask a customer to tell you his orders. So it would be best to have a Repo and : GetOrdersForCustomerId(int id).Amr
Using .SetResultTransformer(new DistinctRootEntityResultTransformer()) for a two level eager load is fine, more than that starts producing cartesian joinsProjective
W
5

I just read Ayende's Blogpost where he used the following Example:

session.CreateCriteria(typeof(Post))
    .SetFetchMode("Comments", FetchMode.Eager)
    .List();

In a Criteria Query to avoid Lazy Loading on one particular Query

Maybe that can help you.

Wallaroo answered 2/12, 2008 at 8:48 Comment(2)
You just saved me a handful of hours regression testing because of this comment. Much obliged.Vtarj
That won't work "over multiple levels" as the questions states. Using FetchMode.Eager for multiple levels will result in a cartesian product. The correct SQL is generated, but NHibernate won't sort it out for you.Discovert
U
1

If you need to keep your one-to-manys as bags, then you can issue 2 queries, each with only 1 level of hierarchy. eg something like this:

var temp = session.CreateCriteria( typeof( Order ) )
    .SetFetchMode( "Lines", NHibernate.FetchMode.Eager )
    .Add( Expression.Eq( "Customer.ID", id ) )
    .List();

var customer = session.CreateCriteria( typeof( Customer ) )
    .SetFetchMode( "Orders", NHibernate.FetchMode.Eager )
    .Add( Expression.Eq( "ID", id ) )
    .UniqueResult();

Lines get loaded into the NH cache in the first query, so they won't need lazy loading when later accessing eg customer.Orders[0].Lines[0].

Uniat answered 24/11, 2011 at 23:54 Comment(0)
W
0

I was having the same problem. See this thread. I didn't get a solution but a hint from Fabio. Use Set instead of bag. And it worked.

So my suggestion is try to use set. You don't have to use Iesi collection use IDictonary and NH is happy

public override IEnumerable<Baseline> GetAll()
{
     var baselines = Session.CreateQuery(@" from Baseline b
                                            left join fetch b.BaselineMilestones bm
                                            left join fetch bm.BaselineMilestonePrevious ")
                                            .SetResultTransformer(Transformers.DistinctRootEntity)
                                            .List<Baseline>();
     return baselines;
}
Wolfish answered 2/12, 2008 at 0:44 Comment(1)
Switching from <bag> to <set> worked for me as well, but a SQL trace shows that a Cartesian product is still produced on the server. This means NHibernate is sorting through and filtering the results on its end, to correctly populate the child and grand-child collections. This probably won't be ideal in many situations, as it may mean thousands or millions of records being dragged across the wire, and then a lot of processing to construct the correct object graph.Discovert
S
0

@Tigraine: your query only returns Post with Comments. This brings All posts with all Comments (2 levels). What Ben asking is Customer to Order To LineItem (3 level). @Ben: to my knowledge nHibernate doesn't support eager loading upto 3 level yet. Hibernate does support it thou.

Spancel answered 2/12, 2008 at 21:23 Comment(1)
@Spancel - I hope you're wrong :-) BUT, if you are right, why would it generate the correct SQL? Luck?Ethbun

© 2022 - 2024 — McMap. All rights reserved.