Thursday, January 8, 2009

NHibernate: What the heck is "clazz_"?

Being a reticent fellow, it isn't often that I blog twice in as many days, but I ran into an issue with NHibernate joined-subclasses and ISQLQuery twice in two different instances in the past two days.

After searching around a good deal for any information I could find about adding entities to SQL queries in NHibernate, I finally found something that pointed me in the right direction, and I thought I'd give it a post to hopefully assist any other unfortunate Hibernate users out there who may encounter this problem.

The Error

You can reproduce this error by creating a class inheritance like:
<class name="BaseEntity" table="BaseEntity">

  <joined-subclass name="FirstChildEntity" table="FirstChildeEntity">

  <joined-subclass name="SecondChildEntity" table="SecondChildeEntity">
Now when you run a SQL query like so:
select * 
  BaseEntity be
  left join FirstBaseEntity fbe on be.Id = fbe.EntityId
  left join SecondBaseEntity sbe on be.Id = sbe.EntityId
We get the following error message.


Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.IndexOutOfRangeException: clazz_

Stack Trace:
[IndexOutOfRangeException: clazz_]
  System.Data.ProviderBase.FieldNameLookup.GetOrdinal(String fieldName) +2307341
  System.Data.SqlClient.SqlDataReader.GetOrdinal(String name) +258
  NHibernate.Driver.NHybridDataReader.GetOrdinal(String name) +33
  NHibernate.Type.NullableType.NullSafeGet(IDataReader rs, String name) +133
  NHibernate.Type.NullableType.NullSafeGet(IDataReader rs, String name, ISessionImplementor session, Object owner) +13
  NHibernate.Loader.Loader.GetInstanceClass(IDataReader rs, Int32 i, ILoadable persister, Object id, ISessionImplementor session) +215
  NHibernate.Loader.Loader.InstanceNotYetLoaded(IDataReader dr, Int32 i, ILoadable persister, EntityKey key, LockMode lockMode, EntityKey optionalObjectKey, Object optionalObject, IList hydratedObjects, ISessionImplementor session) +78
  NHibernate.Loader.Loader.GetRow(IDataReader rs, ILoadable[] persisters, EntityKey[] keys, Object optionalObject, EntityKey optionalObjectKey, LockMode[] lockModes, IList hydratedObjects, ISessionImplementor session) +636
  NHibernate.Loader.Loader.GetRowFromResultSet(IDataReader resultSet, ISessionImplementor session, QueryParameters queryParameters, LockMode[] lockModeArray, EntityKey optionalObjectKey, IList hydratedObjects, EntityKey[] keys, Boolean returnProxies) +285
  NHibernate.Loader.Loader.DoQuery(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies) +756   NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(ISessionImplementor session, QueryParameters queryParameters, Boolean returnProxies) +105
  NHibernate.Loader.Loader.DoList(ISessionImplementor session, QueryParameters queryParameters) +236

The Research

The error message is not very helpful.  All we can really tell from the stack trace is that it's trying to do a field name lookup, but what field is "clazz_"?

If you are like me, you immediately pull open Google and run a search for anything about Nhibernate/Hibernate, clazz_, and createsqlquery.  One of the few useful results is this anotated source code on FishEye, from which we can see that Hibernate uses the "clazz_" string as a column discriminator.  Great!  But for what purpose?

Examining the code a little further, and reading the comments, we can discover that "clazz_" is a column added to the generated sql to differentiate between polymorphic classes, i.e. subclasses.  This is and old version, but it seems we're on the right trail.

A quick search on the forums (which, for some reason, aren't indexed by Google) reveals some 8-10 threads concerning this very issue.  Sadly, it's not the good folks on the Hibernate project who are the most helpful in this case.  Thanks, however, to a fellow by the name of credmond, we can glean a little more info about this "clazz_" column.

To get any further, we're going to have to get our hands dirty.  Let's run a standard HQL query and see what kind of SQL it generates.
Session.CreateQuery("from BaseEntity").List();
The resulting SQL string looks something like this (with most of the fields removed for space):
  entity0_.Id as Id27_, 
  [base entity fields],
  [first child entity fields],
  [second child entity fields],
    when entity0_1_.EntityId is not null then 1 
    when entity0_2_.EntityId is not null then 2 
    when entity0_.Id is not null then 0 
  end as clazz_ 
  BaseEntity entity0_ 
  left outer join FirstChildEntity entity0_1_ on entity0_.Id=entity0_1_.EntityId 
  left outer join SecondChildEntity entity0_2_ on entity0_.Id=entity0_2_.EntityId
We can see here the case statement credmond spoke of.  My first question is: what happens if we change the order of the joined-subclasses in the mapping file?  The resultant SQL looks much the same, except for one important aspect: the aliases in the from clause are reversed!
  BaseEntity entity0_ 
  left outer join SecondChildEntity entity0_1_ on entity0_.Id=entity0_1_.EntityId
  left outer join FirstChildEntity entity0_2_ on entity0_.Id=entity0_2_.EntityId
So it appears that the number associated with each subclass is dependent upon the order of the subclasses in the mapping file.  Thus we find our (ugly) workaround.

The Workaround

We can get our code working (albeit, not very pretty) by adding the "clazz_" discriminator column manually to our SQL:
select *,
    when fbe.EntityId is not null then 1 
    when sbe.EntityId is not null then 2 
    else 0 
  end as clazz_
  BaseEntity be
  left join FirstBaseEntity fbe on be.Id = fbe.EntityId
  left join SecondBaseEntity sbe on be.Id = sbe.EntityId
To make sure this doesn't break down the road, a comment should be added to the mapping file to convey the importance of maintaining the order of the joined-subclass descriptions.

While I don't like this solution, it's the simplest one I've found until the Hibernate team fixes what I consider to be a bug in their implementation of joined-subclasses.


fanglz said...

I just encounter that problem,
Thanks very much!

vuangu said...

I had the same problem, but i used stored procedure. can you give me some idea. I have stuck here for 2 days. T_T. Please help me.

Jimm said...

This was a great post. It solved a great mystery of the universe (for me).

Raja Ranjan Senapati said...

Really Nice Explanation. Solved the exact same problem I was facing.

Mark said...

Since I was just interested in getting base class information from the DB (and the base class was not marked abstract), it was sufficient to write my query like "select base.*, 0 as clazz_ from base" and not doing any joins to subclass tables.