nhibernateentitysubclassicriteria

NHibernate: Query criteria for an entity and possibly a subclass in the same query


This is the setup for my 2 entities:

public class Person {
  public Guid Id {get;set;}
  public string Name {get;set;}
}

public class Immortal : Person {
  public string DarkName {get;set;}
}

Here is what their mappings look like:

<class name="Person">
  <id name="Id">
    <generator class="guid.comb"/>
  </id>
  <property name="Name" />

  <joined-subclass name="Immortal">
    <key column='PersonId' />
    <property name="DarkName" />
  </joined-subclass>
</class>

So that's the setup, 2 entities, one is a joined subclass of the other. I have a search framework that takes any number of criteria from a form and then applies the appropriate criteria to a query, then returns the results.

Now say I have a single form field in this case, "Name" -- I want to bring back all people, be they normal Persons or that special class of beings Immortals, by seeing if their name matches the Name property of Person, but in the case of an Immortal, I'd also like to count it as a match if their DarkName matches what was given in the form.

So this is my dilemma, how do I do this? The best I have been able to do with NHibernate's ICriteria stuff is to make a subquery on Immortal to check the name there, and then see if the root Person id is in that subquery. When working with a table of 10s of thousands of Persons, however, this method of getting results is extraordinarily inefficient and can even time out the request (30s+) in my real world situation.

I would be open to doing this in HQL too, because I figure I want an outer join on Immortal to check for this field, but I cannot get HQL to do a join on 2 disparate entities on an arbitrary property -- joins as far as I know must be based on direct associations in your mappings. For instance, this is what I would like to see:

select person from Person person
  outer join Immortal immortal on immortal.PersonId = person.Id
  where
    person.Name = :name or
    immortal.DarkName = :name

What say you stackoverflow?


Solution

  • The following criteria query produces almost exactly the SQL that you want.

    It takes advantage of some laxness in the criteria query engine.

    var list = session.CreateCriteria<Person>()
        .Add( Expression.Disjunction()
            .Add( Expression.Eq( "Name", name ) )
            .Add( Expression.Eq( "ImmortalName", name ) )
            )
        .List<Person>();
    

    produces the following SQL:

    SELECT this_.Id             as Id0_0_,
           this_.Name           as Name0_0_,
           this_1_.ImmortalName as Immortal2_1_0_,
           case 
             when this_1_.Id is not null then 1
             when this_.Id is not null then 0
           end as clazz_0_
    FROM   person this_
           left outer join immortal this_1_
             on this_.Id = this_1_.Id
    WHERE  (this_.Name = 'foo' /* @p0 */
             or this_1_.ImmortalName = 'foo' /* @p1 */)
    

    I used the following class mapping:

    <class name="Person" table="person">
        <id name="Id">
            <generator class="identity" />
        </id>
        <property name="Name" />
        <joined-subclass name="Immortal" table="immortal">
            <key column="Id" />
            <property name="ImmortalName" />
        </joined-subclass>
    </class>