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> <joined-subclass name="SecondChildEntity" table="SecondChildeEntity"> ... </joined-subclass> </class>Now when you run a SQL query like so:
Session.CreateSQLQuery(@" select * from BaseEntity be left join FirstBaseEntity fbe on be.Id = fbe.EntityId left join SecondBaseEntity sbe on be.Id = sbe.EntityId ").AddEntity(typeof(BaseEntity)).List<BaseEntity>();We get the following error message.
clazz_
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 Hibernate.org 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):
select entity0_.Id as Id27_, [base entity fields], [first child entity fields], [second child entity fields], case 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_ from 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_.EntityIdWe 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!
from 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_.EntityIdSo 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:
Session.CreateSQLQuery(@" select *, case when fbe.EntityId is not null then 1 when sbe.EntityId is not null then 2 else 0 end as clazz_ from BaseEntity be left join FirstBaseEntity fbe on be.Id = fbe.EntityId left join SecondBaseEntity sbe on be.Id = sbe.EntityId ").AddEntity(typeof(BaseEntity)).List<BaseEntity>();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.
6 comments:
I just encounter that problem,
Thanks very much!
Hi,
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.
This was a great post. It solved a great mystery of the universe (for me).
Really Nice Explanation. Solved the exact same problem I was facing.
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.
This was the most frustrating issue I ran into, and it was solved thanks to this post.
Post a Comment