2

I'm trying to get NHibernate to do a simple query based on the inner joins of 3 tables:

var sessionCriteria = session.CreateCriteria<FoobarMaster>("M")
.CreateCriteria("Accounts", "A", NHibernate.SqlCommand.JoinType.InnerJoin)
.CreateCriteria("TrackingRecords", "T", NHibernate.SqlCommand.JoinType.InnerJoin)
.Add(Restrictions.Eq("T.PicNumber", "123456"));
var foobarMaster = sessionCriteria.UniqueResult<FoobarMaster>();

And the same thing in LINQ:

from m in session.Query<FoobarMaster>()
from a in m.Accounts
from t in a.TrackingRecords
where t.PicNumber == "12345"
select m

I have the same thing using QueryOvers and JoinAliases. For all, I'm getting a runtime exception:

"could not resolve property: TrackingNo of: Account".

It is strange since TrackingNo is a TrackingRecord property and not an Account property. It is even prefixed by T - the alias for TrackingRecord.

Here is my mapping:

<class name="FoobarMaster" table="T_FOOBAR_MASTER">
 <id name="FoobarMasterId" column="FOOBAR_MASTER_ID" type="int">
   <generator class="identity"/></id>
 <bag name="Accounts" cascade="all" inverse="true">
   <key column="FOOBAR_MASTER_ID" />
   <one-to-many class="FoobarAccount" />
 </bag>
...

<class name="FoobarAccount" table="T_FOOBAR_ACCOUNT">
 <id name="FoobarAccountId" column="FOOBAR_ACCOUNT_ID" type="int">
   <generator class="identity"/></id>
 <many-to-one name="FoobarMaster" class="FoobarMaster" column="FOOBAR_MASTER_ID" />
 <property name="AccountId" column="ACCOUNT_ID" />
 <bag name="TrackingRecords" cascade="all" inverse="true">
   <key column="ACCOUNT_ID" />
   <one-to-many class="FoobarAccount" />
 </bag>
...

<class name="TrackingRecord" table="T_TRACKING">
 <id name="TrackingId" column="TRACKING_ID" type="int"><generator class="identity"/></id>
 <many-to-one name="FoobarAccount" class="FoobarAccount" column="ACCOUNT_ID" />
 <property name="PicNumber" column="PICNUMBER" type="AnsiString" length="25" />
 ...

Here are classes/Entities:

public class FoobarMaster
{
 public virtual int FoobarMasterId { get; set; }
 public virtual IList<FoobarAccount> Accounts { get; set; }
...

public class FoobarAccount
{
 public virtual int FoobarAccountId { get; set; }
 public virtual FoobarMaster FoobarMaster { get; set; }
 public virtual int AccountId { get; set; }
 public virtual IList<TrackingRecord> TrackingRecords { get; set; }
...

public class TrackingRecord
{
 public virtual long TrackingId { get; set; }
 public virtual FoobarAccount FoobarAccount { get; set; }
 public virtual string PicNumber { get; set; }
...
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
MrTibs
  • 161
  • 1
  • 12
  • Does your LINQ version run ok? can you post your mappings – connectedsoftware Jan 21 '14 at 09:09
  • LINQ version gives the same error. I posted it above and I also posted the mappings. Thanks. – MrTibs Jan 21 '14 at 17:35
  • I don't think it is possible without detached queries like Erik described. I found the following: http://blogs.taiga.nl/martijn/2008/11/20/nhibernate-criteria-queries-across-multiple-many-many-associations/. I'm pretty disappointed. You can do this in Ruby on Rails without breaking a sweat... – MrTibs Jan 23 '14 at 02:00
  • @MrTibs just from curiosity. Have you seen my udpated answer? your mapping is wrong, because `TrackingRecords` has one-to-many ``. This is the problem. Or is the snippet in your question still "adjusted"? Other words, the query, you are trying to execute is CORRECT. I did tested that, once the mapping is correct... I am just curious ;) ;) – Radim Köhler Jan 23 '14 at 07:55

2 Answers2

3

The real issue

The answer here, based on the latest question updated, is clear and easy to fix! The mapping contains a wrong one-to-many setting. See the first level list:

<bag name="Accounts" cascade="all" inverse="true">
  <key column="FOOBAR_MASTER_ID" />
  <!-- here we can see the CORRECT reference -->
  <one-to-many class="FoobarAccount" />
</bag>

On the other hand, the second level is having the same target, which is wrong:

<bag name="TrackingRecords" cascade="all" inverse="true">
  <key column="ACCOUNT_ID" />
  <!--  WRONG. In deed, the Account does NOT contain 'PicNumber' -->
  <one-to-many class="FoobarAccount" />
</bag>

The answer:

Change the <one-to-many class="FoobarAccount" />
into <one-to-many class="TrackingRecord" />

Correct mapping should be like this:

<bag name="TrackingRecords" cascade="all" inverse="true">
  <key column="ACCOUNT_ID" />
  <!--  now we won't recieve the Account does not contain 'PicNumber'  -->
  <one-to-many class="TrackingRecord" />
</bag>

From that moment, all the stuff will work properly, the query in the begining of the question is correct. No need for subqueries etc

...

Original tips - related to previously available information

What you are trying achieve would work if your object/entities would be chained like this

  1. Master has-many (or references) Accounts
  2. Account has-many (or references) TrackingRecords

But based on the issue you have, it seems that your mapping is

  1. Master has-many (or references) Accounts
  2. Master has-many (or references) TrackingRecords

In that case, you can only achieve SQL Like this

select m.* from t_master m
inner join t_account a on m.master_id = a.master_id
//inner join t_tracking t on a.account_id = t.account_id
inner join t_tracking t on m.account_id = t.account_id // the m.account_id
where t.tracking_no = '123456'

And the query should be like this:

// do some filter over A
var rootQuery = session.CreateCriteria<Master>("M")
                       .CreateCriteria("Accounts", "A", NHibernate.SqlCommand.JoinType.InnerJoin);

// working with the T here
rootQuery.CreateCriteria("TrackingRecords", "T", NHibernate.SqlCommand.JoinType.InnerJoin)
         .Add(Restrictions.Eq("T.TrackingNo", "123456"));

Update, reflecting the question extension:

This mapping does not fit together:

The key column of the TrackingRecords is ACCOUNT_ID

<class name="Account" table="T_ACCOUNT"> 
...
<bag name="TrackingRecords" cascade="all" inverse="true">
  <key column="ACCOUNT_ID" />
...

While the reference from TrackingRecord is done via ACCOUNT_NUMBER

<class name="TrackingRecord" table="T_TRACKING">
...
<many-to-one name="Account" class="Account" column="ACCOUNT_NUMBER" />
...
rumblefx0
  • 665
  • 1
  • 9
  • 22
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Thanks Radim, but my setup is like you mention in the former case. Master has many Accounts and Account has many TrakcingRecords. I edited my post and added the classes and mappings. – MrTibs Jan 21 '14 at 17:36
  • I've checked your mapping, and there is at least one inconsistance. Please, check it – Radim Köhler Jan 21 '14 at 17:43
  • Thanks for catching that. I did have the correct mappings (or what I think the correct mappings are), but I had to change the names of some variables when I pasted my code since they contain the name of the product and it is trade marked. I edited the code. – MrTibs Jan 21 '14 at 18:37
  • I would like to help, really, but now I am not sure what is correct mapping, and what is foggy part. Because the latest mapping is weird "ACCOUNT_NUMBER" is a key column of your `TrackingRecords` collection, but this is not mapping the id column name. Summary: The query you have, would in usuall scenario work. I DID Tested that. If the chain is mapped correctly, the query will work. Unless I will se the real code, I can guess as I did until now. Sorry I wanted to help, but this way it is impossible – Radim Köhler Jan 21 '14 at 18:43
  • Again, I appreciate the help. I pasted the exact code and replaced the trademark strings with "foobar". – MrTibs Jan 21 '14 at 20:33
  • I've updated my answer, I would say, now we do have the culprit. The point is, that with correct mapping, your query is working. If I did tested that! ;) – Radim Köhler Jan 22 '14 at 07:56
  • Dude, you are my savior! I showed this problem to 3 co-workers and nobody noticed that issue. I also had to add a property-ref="AccountId" on the key node, but other than that it worked like a charm. Thanks so much! – MrTibs Jan 23 '14 at 19:05
  • Haleluja, great to see that! amazing ;) NHibernate is simply awesome ;) Good luck, sir – Radim Köhler Jan 24 '14 at 06:13
0

Since you only select your Master data through SQL, with no fields/columns from T_Account and T_TrackingRecord, I would recommend to do it with DetachedCriteria and Subqueries.

If you had TrackingRecord.AccountId and Account.MasterId properties, you could do like this:

var trackRecAccountIdsSubq = DetachedCriteria.For(typeof(TrackingRecord))
                .SetProjection(Projections.Property("AccountId"))
                .Add(
                    Restrictions.Eq("TrackingNo", "123456")); 

var accountMasterIdsSubq = DetachedCriteria.For(typeof(Account))
                .SetProjection(Projections.Property("MasterId"))
                .Add(
                    Subqueries.PropertyIn("AccountId", trackRecAccountIds)); 

var mastersByTrackRecNoCriteria = session.CreateCriteria<Master>()
                .Add(
                    Subqueries.PropertyIn("MasterId", accountMasterIdsSubq);

var mastersByTrackRecNo = mastersByTrackRecNoCriteria.List<Master>();

Generated SQL will be like:

Select * from t_master where master_id in (
    select master_id from t_account where account_id in (
        select account_id from t_tracking where tracking_no=123456
    )
)

Not sure if and how this works with the entity object properties (without foreign ID properties), but subqueries create fast semi joins on the database (ideally through an index on the parent entity ID).

This also does, by default, fetch only the Master, not the Account or TrackingRecord; good and faster if you don't need them. If you need them, and you have only one Account and Master per TrackingNo, a join fetch might be the better choice.

Erik Hart
  • 1,114
  • 1
  • 13
  • 28
  • @MrTibs: Most DBMS translate subselects into semi-joins, these can be internally done by nested queries (just like normal inner joins, too), or other algorithms, depending on what the optimizer chooses. If you have exactly 1 Account and Master per TrackingNo, this is quite likely and not bad at all. Advantage is: you don't select the children, and you would not get multiple equal Masters, if different join paths lead to it. – Erik Hart Jan 21 '14 at 18:50
  • I got it to work with detached queries but I hate the look of the code. I thought the point of an ORM is to simplify things, not to make things harder. Not everyone in my company is a NHibernate expert. If I leave that code in, they won't know what I'm trying to do. I ended up writing raw SQL. Thanks anyway. – MrTibs Jan 23 '14 at 02:02