Blog from November, 2011

NHibernate- why sharing index columns is a bad idea

Consider the following mapping:

Mapping of Map and GroupLayer
<class name="SharpMap.Layers.GroupLayer, SharpMap" table="grouplayer">
        <list name="Layers" lazy="false" cascade="all" collection-   type="DeltaShell.Plugins.Data.NHibernate.Collections.Generic.PersistentEventedListType`1[[SharpMap.Layers.ILayer, SharpMap]], DeltaShell.Plugins.Data.NHibernate">
          <key column="parent_layer_id"/>
          <index column="list_index" />
          <one-to-many class="SharpMap.Layers.ILayer, SharpMap"/>
        </list>
 </class>

 <class name="Map" table="map" lazy="false" >
    <id name="Id" column="id" type="Int64">
      <generator class="native" />
    </id>
    <list name="Layers" lazy="false" cascade="all-delete-orphan" collection-type="DeltaShell.Plugins.Data.NHibernate.Collections.Generic.PersistentEventedListType`1[[SharpMap.Layers.ILayer, SharpMap]], DeltaShell.Plugins.Data.NHibernate">
      <key column="map_id"/>
      <index column="list_index" />
      <one-to-many class="SharpMap.Layers.ILayer, SharpMap"/>
    </list>
</class>

As you can see both grouplayer and Map have a list of layers (ILayer) when are mapped in a <list> element. This will result in a 'schema' like this:

Since a layer is in not the collection of map layers and in a grouplayer at the same time the list_index could be shared right??? Unfortunately this is not the case. Consider the following:
1 Add a map
2 Add a grouplayer to the map
3 To that grouplayer add another layer
4 Save / Load
5 Now move the layer of 3 out of the grouplayer into the map of 1.
6 Save / Load
7 An expcetion will occur: something like 'null index column for collection: SharpMap.Map.Layers'

Now what is happening? During the save of 6 we see the following sql (with NHibernate Profiler)

UPDATE layer
SET    parent_layer_id = null,
       list_index = null
WHERE  parent_layer_id = 2 /* @p0 */
       AND id = 6 /* @p1 */

This is were NHibernate is removing the layer out of the grouplayer. NHibernate sets the list_index to null and therefore messes up Maps->Layers relationship. As NHibernate considers the relationships unrelated this is should be valid.

So....for every relationship use a separate index column. It is a good convention to use the same prefix as used for the key column. The mapping of grouplayer then looks like this:

Mapping of GroupLayer
<class name="SharpMap.Layers.GroupLayer, SharpMap" table="grouplayer">
        <list name="Layers" lazy="false" cascade="all" collection-   type="DeltaShell.Plugins.Data.NHibernate.Collections.Generic.PersistentEventedListType`1[[SharpMap.Layers.ILayer, SharpMap]], DeltaShell.Plugins.Data.NHibernate">
          <key column="parent_layer_id"/>
          <index column="parent_layer_list_index" />
          <one-to-many class="SharpMap.Layers.ILayer, SharpMap"/>
        </list>
 </class>

Whenever you put an object in a DataItem, NHibernate figures out how to save this 'Value' entity using what is called an Any mapping. As part of this Any mapping mechanism, NHibernate stores the (CLR, .NET) type of the object in a column in the database as fully qualified string. Also sometimes we map types directly, for example ValueType of a dataitem.

This is all fine, until you rename a class..

When NHibernate tries to convert the string back into a .NET type, it will fail and crash. Legacy mappings won't help you there.

To solve this issue, we have added a custom HBM (meta)attribute in DeltaShell: oldClassName. The value of this attribute is used as an alias when resolving types. This happens automagically for the any mapping of dataitem, but if you have mapped a Type column/property directly, make sure to load it using TypeUserType in your legacy mapping.

Example:

Your old class 'Branch' has been renamed to 'Channel'. Now in your legacy mapping you do the following:

Legacy Mapping
  <class name="Channel" table="branch" lazy="false">

    <meta attribute="oldClassName">Domain.Branch, Domain</meta>

    <id name="Id" column="Id" type="Int64" unsaved-value="0"><generator class="native" /></id>
    ...
  </class>

The value of the 'oldClassName' attribute is the string as it appears in the old database, typically 'namespace.class, assembly'.

To summarize: if you rename a persistent class, and there is any chance the object has ever been used as DataItem value, parameter, variable, filter, or had its type mapped directly for any other reason: make sure to add a meta attribute with the old class alias.

Caveats of lazy-loading in NHibernate

NHibernate's lazy functionality is a good way of postpone retrieval of an object and therefore increase performance. However there are a few caveats one should be aware of:

  1. Your need a custom equals. If you compare a 'lazy' with the underlying 'real' object you want them to be equal. To implement this you need a custom Equals implementation. In DS you can use Unique as a base class. This implements a correct equals for you.
  2. If the object you want to load does update on changes of another object (propertychanged etc) you have a problem. For example : You lazily load a network coverage. Normally when a branch is removed from the network the coverage auto-removes the values for this branch. BUT when the coverage is lazy-loaded it does not get notified of the branch removal and does not update. When the coverage is unproxied it will still contain locations of the removed branch. In cases where there is no such relationship ,for example a timeseries of a lateral there is no problem and this function can be lazy-loaded.