Mike Nichols - Son of Nun Technology

March 2007 Entries

Understanding Lazy Loading Strategies for NHibernate

For some time I have put off implementing lazy-loading in my current project because (ironically) I am lazy. It is just conceptually easier to load the object you want and then move on. I think this due to a brief encounter I had with a NHibernate-created frankenproxy when I issued a 'ToString()' on one of the objects I had loaded and as far as I recollect I got the crazy proxy name instead of the override ToString() value I expected. Now I am sure now I had simply not overriden ToString() like I thought I had but I simply haven't had time to sit down and design as if my objects were lazy loaded.The notes here are what I picked up today so if you see something wrong please let me know!

The proxied instances returned by NHibernate will invoke the ToString(), Equals(), and GetHashCode() methods to the target object type if they have been overriden (which I hope is the case). This might be a concern since the proxies themselves would have these methods, but they are transparent in this regard.

The newer versions of NHibernate load lazily by default, so it is important to have made decisions and understand what your Unit of Work strategy is. Assuming this is an ASP.NET application, are you storing your ISession in the Context.Items and having the end of the Request flush your ISession? ( a common practice) This is relevant because it is important to know that if you are working with objects that have been loaded lazily after the session has been flushed will throw an NHibernate exception since the proxy can't invoke to its target without an ISession. If this comes up, you may reattach the object to the current ISession using session.Lock(myObject) . Personally, I use FlushMode.Commit mode since I like things to happen when I instruct them to and it is conceptually easier for me than to tie my .Flush() to an event.

There are a couple of options for determining the objects NHibernate will return from its lazy-loads.

#1

The most common is to simply mark the class with the 'lazy="true"' attribute or place 'default-lazy="true"' in the mapping declaration:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="@core.assembly@"

                  default-access="nosetter.camelcase-underscore" default-lazy="true">

Or

  <class name="Cei.eMerge.Core.Domain.Contacts.Contact" table="Contact" lazy="true" >

If you go this route, remember the following:

  • Have a default constructor that is at least protected visibility. Private will not be able to be proxied.
  • Properties and methods will need to be marked virtual. This is because the proxy returned by NHibernate is a subclass of your object. This point might make you hesitate to go the lazy-loading route, but don't let it. The DDD purist might shudder because the persistence mechanism is somehow influencing the domain objects (of course the default constructor NHibernate requires is another violation of this). This is one of those decisions where your business objectives need to drive your decision instead of your technology objectives...it might make sense to avoid this, but be sure you weigh the performance benefit of lazy-loading in there, too.
  • This route allows you to use field access strategy for things like read-only collections and so on. I explain this in the next section.

#2

The other option is to tell NHibernate what interface to return for your proxy using the 'proxy="IMyInterface"' in the class declaration:

  <class name="Cei.eMerge.Core.Domain.Contacts.Contact" table="Contact" proxy="IContact" >

Remember this is you go this route:

Properties and methods don't need to be marked virtual.

  • This will impact the access strategies available to your objects' members. If you were to use 'access="field.camelcase-underscore"'  or 'access="nosetter.camelcase-underscore"' for one of the properties above Nhibernate will try to find a member such a name as "_fieldName" and will throw an exception because that doesn't exist in your interface. One solution would be to change the access strategy to "access="property"" (the default) and allow for an setter that has a "lower" visibility (C# 2.0 and higher only). That way, the visiblity is still restricted but NHibernate can set your properties' values. But wait...here again we are letting persistence decisions trickle into our domain objects. That may be fine, but it should still be a deliberate decision. For example, on the Person implementation of IPerson we might have:

            /// <summary>

            /// The First Text of an Individual Contact. Such as "Franz" or "Harry".

            /// </summary>

     

            public virtual string FirstName

            {

                get

                {

                    return _firstName;

                }

                protected set{ _firstName = value;}

            }

  • A final common issue is casting. If you lazyload an object and try to cast it to its inherited type, you will get a cast exception. Remember you are dealing with a proxied instance which is a subclass of your intended object...thus the casting exception.

The benefits of lazy-loading can be immense. Not only can you avoid the common select n+1 problem, but you can drastically reduce the size of the queries hitting your db. This will especially be important if you have, say, turned ViewState off and are hitting the DB for data with each request and caching isn't an option.

UPDATE: I goofed when I wrote this (late at night). Select N+1 is something to beware of when dealing with lazy loading. See this post by Oren Eini for ways to deal with this. Thanks Bill for catching my error!

SqlCE Subqueries and NHibernate Unit Testing

Using NHIbernate ICriteria , I created a DetachedCriteria that looked like so:

 

                DetachedCriteria innerAssociate = DetachedCriteria.For(typeof (Contact))

                    .SetProjection(Projections.Property("Id"))

                    .Add(Property.ForName("Id").Eq(idToFind))

                    .SetProjection(Projections.Property("Id"))

                    .Add(Property.ForName("Id").EqProperty("associate.Id"));

 

 

                DetachedCriteria parentSub = DetachedCriteria.For(typeof(ContactAssociation), "parentSubAss")

                    .SetProjection(Projections.Property("Id"))

                    .Add(Expression.Eq("AssociationType",ContactAssociationTypes.PARENT))

                    .CreateCriteria("Associate", "associate")

                    .Add(Property.ForName("Id").Eq(innerAssociate));

 

 

                DetachedCriteria parentCrit = DetachedCriteria.For(typeof(Contact), "parentCon")

                    .CreateCriteria("ContactAssociations","association")

                    .Add(Subqueries.Exists(parentSub));

The interesting part of the SQL generated by this is this:

 


FROM Contact this_

inner join ContactAssociation associatio1_ on this_.Id=associatio1_.ContactId

left outer join Contact contact4_ on associatio1_.Associate_ContactId=contact4_.Id

WHERE exists

(SELECT this_0_.Id as y0_ FROM ContactAssociation this_0_

inner join Contact associate1_ on this_0_.Associate_ContactId=associate1_.Id

WHERE this_0_.AssociationType = ? and associate1_.Id =

(SELECT this_0_0_.Id as y0_ FROM Contact this_0_0_ WHERE this_0_0_.Id = ? and this_0_0_.Id = associate1_.Id))]

You can see that I have a nested subquery within a primary subquery. From what I have gathered about SqlCE, it should supported unlimited nested subqueries, so I am confused why the SQL generated by NHibernated would create this error:

System.Data.SqlServerCe.SqlCeException: There was an error parsing the query. [ Token line number = 1,Token line offset = 1777,Token in error = SELECT ]

The snippet is is indicating it dislikes is this and associate1_.Id = (SELECT this_0_0_.Id as y0_ FROM Contact... from the second subquery.

I only show this to point out a potential failure for unit tests using NHibernate when, in fact, the sql is just fine.

Estimating Gives Me Gas

Hammett has a experience he had on a recent post that was refreshing for me to read. I love hearing about real-life experience developers have because I work alone and so am always thinking everyone else out there doesn't encounter the same quandries I do. Software estimation has been one of these quandries for me mostly because of my lack of experience. It is frankly nice to hear that brilliant developers like Hammett have at least somewhat similar difficulties I come across.

Some background:

I work for a firm that has a fleet of project managers doing construction administration and related tasks for the construction industry. What this means is a large number of 'bonified' engineers who are very used to having project estimates and cost proposals that, while subject to change, are expected to be pretty close to the mark. Now, I have been afforded grace by them for my inability to meet the unrealistic expectations ANY new developer would set. You know, it is easy to confuse being able to CONCEIVE of a solution to the problem with the actual IMPLEMENTATION of it, so I would 'paint myself into a corner' with my own inexperience.

As time has gone on, agile methodology has been a pursuit of mine (tho I have very far to go in being consistent with its practices) and I have found that while my short-term (read:iterative) estimation has gotten very accurate (since I try to stay at one-week and two-week iterations of new features), answering questions such as 'Program X which my software is replacing will be totally obsolete in 3 months' has remained elusive.

It is easy to sum up the problem by saying the owners 'don't understand technology or software development' and pretend their expectations are unreasonable, but I think part of our job is to HIDE complexity from folks who don't know software since they otherwise wouldn't need us. Granted, I haven't read the resources I am sure are out there for improving this because frankly I haven't had time and it is difficult to lay down improving my chops at development to improve my chops on giving the people who pay for my ferrari (ok...it's an '85 CJ7) meaningful and accurate answers.

So when my owners look at me from across the table and want answers that they are used to receiving from guys who build roadways and I can mostly just say 'well, in a week we'll have contact group features completed, but I have no idea when you'll be able to cut an invoice', I kind of feel like the guy who has really bad gas and just wants to leave the room as soon as possible.

:)

MIKE