32

I'm bad at math but I kind get idea what cartesian product is.
Here is my situation (simplified):

public class Project{
 public IList<Partner> Partners{get;set;}
}
public class Partner{
 public IList<PartnerCosts> Costs{get;set;}
 public IList<Address> Addresses{get;set;}
}
public class PartnerCosts{
 public Money Total{get;set;}
}
public class Money{
 public decimal Amount{get;set;}
 public int CurrencyCode{get;set;}
}
public class Address{
 public string Street{get;set;}
}

My aim is to effectively load entire Project.

Problem of course is:

  • If I try to eager load partners and their costs, query returns gazillion rows
  • If I lazy load Partner.Costs, db gets request spammed (which is a bit faster than first approach)

As I read, common workaround is to use MultiQueries, but I kind a just don't get it.
So I'm hoping to learn through this exact example.

How to effectively load whole Project?

P.s. I'm using NHibernate 3.0.0.
Please, do not post answers with hql or string fashioned criteria api approaches.

Arnis Lapsa
  • 45,880
  • 29
  • 115
  • 195
  • 2
    I don't think you will get a cartesian product here. Your structure is Project-1:n-Partner-1:n-PartnerCosts-1:1-Money. So the number of rows you get in your result will always be count(PartnerCosts). You would get a cartesian product if you had another IList in your Partner class and tried to load that in the same query. Then you would get count(Something) * count(PartnerCosts). Since you don't want ICriteria or HQL your best option would be QueryOver with Futures. I'll write up an example for that later and post that as an answer if no one else will have done that by then. – Florian Lim Mar 11 '11 at 13:14
  • @Florian as I said - I'm bad at math. revised my understanding a bit and added `Addresses` for partner. Usage of `QueryOver` would be perfect. – Arnis Lapsa Mar 11 '11 at 15:09
  • Please Help. This is not working for me and I need to see how you are doing ".JoinAlias(p => p.Project, () => pAlias)" When there is no property for project on the project class??? Were the classes you used the exact same as the ones posted in the question? How does p.Project even compile? – joncodo Jan 12 '12 at 19:25
  • @JonathanO "When there is no property for project on the project class" - doubt that project should self reference itself. If you mean "partners have no reference to project", then - I'm afraid this approach fighting cartesian product won't work. But I really don't remember, doubt that I will be able to help. :) – Arnis Lapsa Jan 12 '12 at 19:43
  • Join me here in chat http://chat.stackoverflow.com/rooms/6667/question – joncodo Jan 12 '12 at 19:50

4 Answers4

47

Ok, I wrote an example for myself reflecting your structure and this should work:

int projectId = 1; // replace that with the id you want
// required for the joins in QueryOver
Project pAlias = null;
Partner paAlias = null;
PartnerCosts pcAlias = null;
Address aAlias = null;
Money mAlias = null;

// Query to load the desired project and nothing else    
var projects = repo.Session.QueryOver<Project>(() => pAlias)
    .Where(p => p.Id == projectId)
    .Future<Project>();

// Query to load the Partners with the Costs (and the Money)
var partners = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(p => p.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Costs, () => pcAlias)
    .JoinAlias(() => pcAlias.Money, () => mAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// Query to load the Partners with the Addresses
var partners2 = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(o => o.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Addresses, () => aAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// when this is executed, the three queries are executed in one roundtrip
var list = projects.ToList();
Project project = list.FirstOrDefault();

My classes had different names but reflected the exact same structure. I replaced the names and I hope there are no typos.

Explanation:

The aliases are required for the joins. I defined three queries to load the Project you want, the Partners with their Costs and the Partners with their Addresses. By using the .Futures() I basically tell NHibernate to execute them in one roundtrip at the moment when I actually want the results, using projects.ToList().

This will result in three SQL statements that are indeed executed in one roundtrip. The three statements will return the following results: 1) 1 row with your Project 2) x rows with the Partners and their Costs (and the Money), where x is the total number of Costs for the Project's Partners 3) y rows with the Partners and their Addresses, where y is the total number of Addresses for the Project's Partners

Your db should return 1+x+y rows, instead of x*y rows, which would be a cartesian product. I do hope that your DB actually supports that feature.

Florian Lim
  • 5,332
  • 2
  • 27
  • 28
  • This looks like pure gold. Will mark as answered once tested. Thank You very much! :) – Arnis Lapsa Mar 12 '11 at 21:58
  • Thanks for fixing the example Florian - this really helped me out. – David McClelland Oct 20 '11 at 14:05
  • This is not working for me and I need to see how you are doing ".JoinAlias(p => p.Project, () => pAlias)" When there is no property for project on the project class??? Were the classes you used the exact same as the ones posted in the question? How does p.Project even compile? – joncodo Jan 12 '12 at 19:25
  • 1
    @JonathanO Sorry for the late reply. `p.Project` compiles, because `p.Project` is not a property of the `Project` class but a property of the `Partner` class. In the expression `.JoinAlias(p => p.Project, () => pAlias)` the `p` references the class from `.QueryOver`. – Florian Lim Jan 17 '12 at 08:13
  • I am also confused. The Partner class only has two properties: Costs and Addresses. Project is not a property of the Partner class. – Jacko Feb 24 '12 at 21:26
  • 1
    @Jacko It does not appear in the original question, but in my example code I had a reference (in the class and in the mapping) from `Partner` to `Project`. In short, in the `Project` class there is an `public IList Partners{get;set;}` and in the `Partner` class we have `public Project Project {get;set;}`. The mapping file looks accordingly. – Florian Lim Feb 28 '12 at 14:34
  • @Florian If there is a condition, for e.g. PartnerCosts.Total.Amount should be greater than $0, where would you put it? In "projects" query or all the three? – Dharmesh Dec 07 '13 at 09:20
  • @Dharmesh If your goal is "load the project with id x and only the PartnerCosts with Amount greater 0" then I would try to put that in the second query (partners), but not in all three. I don't have that test code around anymore so I can't test that myself. – Florian Lim Dec 11 '13 at 10:16
  • I tried this today and there is a change needed (might be due to NHibernate changes since this was written). The first query needs to eager load the partners otherwise NHibernate will issue an additional query `var projects = repo.Session.QueryOver(() => pAlias) .Where(p => p.Id == projectId).Fetch(p=>p.Projects()).Eager.Future();` – Konstantin Feb 06 '14 at 12:19
  • @FlorianLim If i want to use skip & take to projects queryover , will it affects performance – Sajjad Ali Khan Sep 13 '17 at 13:00
5

If you are using Linq on your NHibernate, you can simplify cartesian-prevention with this:

int projectId = 1;
var p1 = sess.Query<Project>().Where(x => x.ProjectId == projectId);


p1.FetchMany(x => x.Partners).ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Costs)
    .ThenFetch(x => x.Total)
.ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Addresses)
.ToFuture();


Project p = p1.ToFuture().Single();

Detailed explanation here: http://www.ienablemuch.com/2012/08/solving-nhibernate-thenfetchmany.html

Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • This works, but only because we have one child collection off of project (and hence, the FetchMany(x => x.Partners) is ok. If there is a second child collection (say Contributers or something), this would still result in a cartesian product, as you'd need to call FetchMany on Project for both child collections. It appears NH LINQ isn't smart enough to tie child collection future queries to the root entity, like QueryOver apparently can. See: http://stackoverflow.com/a/5435464/201308 – kdawg Feb 22 '13 at 16:30
  • @kdawg is Contributors a child of Project? or child of Partners? I'm interested to repro that cartesian product – Michael Buen Feb 23 '13 at 00:03
  • a child of Project. Actually, I want to revisit this again. Originally, I had something like "sess.Query().FetchMany(x => x.Partners).FetchMany(x => x.Contributers).ToFuture()", which naturally produced a cartesian project. Since then, I've had a crash course in NH Futures and ended up with something very similar to what you have above, but with the QueryOver API. I originally thought LINQ futures couldn't do what I wanted it to do (a similar issue to this), but now I'm not quite sure anymore! =) – kdawg Feb 23 '13 at 04:56
  • first Linq example I've found that actually works. Thank you!! – Simon Fox Apr 30 '15 at 22:34
2

Instead of eager fetch multiple collections and get a nasty Cartesian product:

Person expectedPerson = session.Query<Person>()
    .FetchMany(p => p.Phones)
        .ThenFetch(p => p.PhoneType)
    .FetchMany(p => p.Addresses)
    .Where(x => x.Id == person.Id)
    .ToList().First();

You should batch children objects in one database call:

// create the first query
var query = session.Query<Person>()
      .Where(x => x.Id == person.Id);
// batch the collections
query
   .FetchMany(x => x.Addresses)
   .ToFuture();
query
   .FetchMany(x => x.Phones)
   .ThenFetch(p => p.PhoneType)
   .ToFuture();
// execute the queries in one roundtrip
Person expectedPerson = query.ToFuture().ToList().First();

I just wrote a blog post about it which explains how to avoid that using Linq, QueryOver or HQL http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies/

Jarrad
  • 927
  • 8
  • 19
Raffaeu
  • 6,694
  • 13
  • 68
  • 110
  • Changed, please upgrade my post – Raffaeu Jul 04 '14 at 06:20
  • Great post, I'm stuck with NHibernate 3.x on an old project (we use Visual Studio 2008, for God's sake). I can't even use Query with ToFuture due to a nasty bug, had to conform with QueryOver. Your post saved my day. – Tiago César Oliveira Jul 05 '16 at 14:37
1

I just wanted to contribute to the really helpful answer by Florian. I found out the hard way that the key to all of this is are the aliases. The aliases determines what goes in to the sql and are used as "identifiers" by NHibernate. The minimal Queryover to successfully load a three level object graph is this:

Project pAlias = null;
Partner paAlias = null;

IEnumerable<Project> x = session.QueryOver<Project>(() => pAlias)
 .Where(p => p.Id == projectId)
 .Left.JoinAlias(() => pAlias.Partners, () => paAlias)
 .Future<Project>();


session.QueryOver(() => paAlias).Fetch(partner => partner.Costs).
 .Where(partner => partner.Project.Id == projectId)
 .Future<Partner>();

The first query loads project and its child partners. The important part is the alias for Partner. The partner alias is used to name the second query. The second query loads partners and costs. When this executed as a "Multiquery", Nhibernate will "know" that the first and second query are connected by the paAlias (or rather the sqls generated will have column aliases that are "identical"). So the second query will continue the loading of Partners that was already started in the first query.

Konstantin
  • 3,626
  • 2
  • 33
  • 45