I am using Nhibernate for my ORM.
I have a class "Control" that has a one to many relationship with ControlDetail (ie. A control has many controlDetails).
In the control xml config it has the following
<bag name="ControlDetails" lazy="true" access="property" order-by="SortOrder asc" cascade="all-delete-orphan"
table="ControlDetail">
<key column="ControlID"/>
<one-to-many class="ControlDetail"/>
</bag>
such that I believe unless otherwise told it would lazy load the controldetails of a control.
I am running NHProf to try and fix some performance issues we are having and it has identfied a Select N + 1 issue around these classes.
We are using a repository DA layer and I have tried to see if I can add in a way to eagerly fetch the data when required and came up with this.
public T GetById<T>(Int32 id, List<string> fetch) where T : BaseObject
{
T retObj = null;
ISession session = EnsureCurrentSession();
{
ICriteria criteria = session.CreateCriteria(typeof (T));
criteria.SetCacheable(true);
criteria.Add(Expression.Eq("Id", id));
foreach(var toFetch in fetch)
{
criteria.SetFetchMode(toFetch, FetchMode.Eager);
}
retObj = criteria.List<T>().FirstOrDefault();
}
return retObj;
}
*Note: I'm not a fan of how the repository is setup but it was done before I came to the project so we have to stick with this pattern for now.
I call this method like so
public Control GetByIDWithDetail(int controlID)
{
return DataRepository.Instance.GetById<Control>(controlID, new List<string>() {"ControlDetail"});
}
When I debug the GetByID method and look at the retObj I can see that the ControlDetails list has been populated (although strangely enough I also noticed without the setfetchmode set the list was being populated)
Even with this fix NHProf identifies a Select N+1 issue with the following line
List<ControlDetail> details = control.ControlDetails.ToList();
What exactly am I missing and how can I stop this N+1 but still iterate over the controlDetails list
EDIT: the xml configs look like so (slightly edited to make smaller)
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="DomainObjects" assembly="DomainObjects">
<class name="Control" lazy="false" table="Control" optimistic-lock="version" select-before-update="true" >
<id name="Id" type="int" column="ControlID" access="property">
<generator class="native" />
</id>
<version name="Version" column="Version" />
<property name="AdministrativeControl" column="AdministrativeControl" access="property" not-null="true" />
<property name="Description" column="ControlDescription" access="property" />
<property name="Title" column="Title" access="property" />
<property name="CountOfChildControls" access="property" formula="(select count(*) from Control where Control.ParentControlID = ControlID)" />
<bag name="ControlDetails" lazy="true" access="property" order-by="SortOrder asc" cascade="all-delete-orphan"
table="ControlDetail">
<key column="ControlID" />
<one-to-many class="ControlDetail" />
</bag>
</class>
</hibernate-mapping>
and this
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="DomainObjects" assembly="DomainObjects">
<class name="ControlDetail" lazy="false" table="ControlDetail" select-before-update="true" optimistic-lock="version">
<id name="Id" type="int" column="ControlDetailID" access="property">
<generator class="native" />
</id>
<version name="Version" column="Version" />
<property name="Description" column="Description" access="property" not-null="true" />
<property name="Title" column="Title" access="property" />
<many-to-one name="Control" lazy="false" class="Control" column="ControlID" access="property"/>
</class>
</hibernate-mapping>
One option, you can reduce the select n+1 problem, keep lazy loading and forget about eager loading....
All you need to do is to add one simple attribute
batch-size
to your XMLI also noticed that you have
lazy="true"
in your mapping, did you try changing to false?There is a substantial difference between eager fetching and eager loading. Fetching means: putting into the same query. It has several side effects and may break it. Eager loading means forcing NH to load it immediately, not waiting until it is accessed the first time. It is still loaded using additional queries, which leads to the N+1 problem.
NH probably does not eagerly fetch because the property is called
ControlDetails
, but you passControlDetail
as argument.On the other side, this isn't a good way to avoid the N+1 problem. Use batch-size instead. It is fully transparent to the application and reduces the amount of queries by the given factor (values from 5 to 50 make sense, use 10 if you don't know what to use).