Question

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; }
...
Was it helpful?

Solution

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" />
...

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top