ORM with NHibernate

Published 12 May 08 12:33 PM | jons 

In this post I am going to refer to the code and Power Point slides that I used at the Code Camp.  You do not need to have these materials but it might help.

Just to refresh our memories, an ORM maps data between the application domain model objects and the relational database schema.  The domain model objects and the database schema are (or should be) pretty commonplace concepts.  The only wildcard is how to specify the mapping.  This post will cover the several mechanisms that NHibernate provides to specify the mapping. 

(As an aside, the word mechanism strikes me as odd.  It derives from physical machines, but I end up using it quite often to describe digital things such as software programs.  May be we need a word like "digitalism".  Hmmm.) 

I am going to present the different digitalisms in order from least flexible to most flexible.  In each approach, we need to define two basic elements for each of the domain model objects:  First, we need to specify the base table that holds the data elements.  This is the table against which the CRUD (Create, Read, Update, and Delete) operations are performed.  Other tables may be involved, but most of the data elements within the typical domain object are associated with a single (base) table.  I will call this the Table mapping.  Second, we need to specify, for each data element, the mapping between the data element and a corresponding data column (or set of columns or even a part of a column).  I will call this the Column mapping.

Embedded in the Code: NHibernate provides a means to define the Table and Column mappings as annotations.  The table annotation appears as an annotation of the Class declaration:

   1: [Class](Table:="ProgrammingLanguages", Lazy:=False)> _
   2: Public Class ProgrammingLanguage

Each data element may also be annotated.  Here, there is a choice.  The Big Book of Best Practices says that [cue the drum roll, cue the announcer with the basso profundo voice] you should code the data element as a private (or perhaps protected) field, wrapped in get and set property methods.  I rarely do this any other way, but it can cause problems.  I like the objects to be as immutable as possible; the fewer points in the code that can change something, the better.  What this often means is that there are quite a few properties with no setter property methods.  That poses a challenge to the ORM mechanism: how does it get the data into the object when it creates it.  While the class might define a series of factory methods and constructors that provide the mechanisms (digitalisms) to set the values, it is almost certain that these mechanisms are too specific for the ORM to use.  Accordingly, most ORM packages use reflection to gain access to the private fields.  [And, yes it is slower than straight access but the slowdown from reflection is so much less than the time it takes to move the data between the application and the database; it is not going to be a problem.]  If the mapped class does not have setter methods on some of the data elements, you can apply the annotations to the private fields.  NHibernate will find and set the values.  Otherwise, you can apply the annotations on the public property.  Actually, I believe that NHibernate requires the annotations for a class to be all one or the other.  Thus, if you have immutable properties, you must apply the annotations to the fields rather than to the properties.

   1: <[Property](Name:="mLanguageTitle", Column:="Title", Length:=30, NotNull:=True, Access:="field")> _
   2:  Private mLanguageTitle As String

As an observation, while this is the easiest way to define the mapping, this is the least flexible approach.  The annotations are in the code and to make any changes to the mapping values you would have to change the code and re-compile.  What you have to do here is to decide how likely it is for the mapping to change and what the consequences of that change might be.  In my sample application, I included a table of programming languages with a character-based identifier and a title.  This is dead simple and is somewhat unlikely to change.  But it could change: there I would be with the hammer and chisel making my changes.  If the organization is properly paranoid, it might insist that any code change must trigger a full Q/A cycle.  My recommendation is to use the annotations only during the initial development (during the time that the domain model is evolving with some vigor) and transition to one of the next techniques before the first truly serious deployment.

Specification in XML Files.  All of the remaining approaches specify the mapping in XML files.  NHibernate provides two different sets of files.  The first file is the configuration file that defines the connection to the database and identifies the mappings for the individual class/table associations.  This configuration data can be in the standard application configuration or in a separate configuration file that the standard configuration file identifies.  Here is an example of the separate configuration file:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   3:    <session-factory>
   4:       <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
   5:       <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
   6:       <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   7:       <property name="connection.connection_string">Server=.;Database=OrmTest;Integrated Security=true</property>
   8:       <property name="show_sql">true</property>
   9:       <mapping assembly="NXCSLAEmployeeAssignmentsLib" />
  10:       <mapping file="ProjectResourceNeed.hbm.xml" />
  11:       <mapping resource ="ManageAssignments.Task.hbm.xml" assembly="ManageAssignments" />
  12:    </session-factory>
  13: </hibernate-configuration>

Lines 4 through 8 in this file tell NHibernate that the database is a MS SQL 2005 database named ORMTest on an instance of SQL Server that is running on my local laptop.

The NHibernate specifications for each class are stored in separate XML files.  Here is a somewhat complicated example from the sample application:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
   3:     assembly="NXCSLAEmployeeAssignmentsLib"
   4:     namespace="NXCSLAEmployeeAssignmentsLib">
   5:    <class name="ProjectResourceNeed" lazy="false" mutable="false" table="Tasks">
   6:       <id 
   7:          name="mResourceNeedID" 
   8:          column="TaskID" 
   9:          access="field">
  10:          <generator class="assigned" />
  11:       </id>
  12:      <property 
  13:         name="mTaskTitle"
  14:         access="field"
  15:         column="Title" 
  16:       />
  17:       <property 
  18:          name="mResourceStartOnDate"
  19:          type="Nullables.NHibernate.NullableDateTimeType, Nullables.NHibernate"
  20:          access="field"
  21:          column="StartDate"
  22:       />
  23:       <property 
  24:          name="mResourceEndOnDate"
  25:          type="Nullables.NHibernate.NullableDateTimeType, Nullables.NHibernate"
  26:          access="field"
  27:          column="EndDate"
  28:       />
  29:       <property name="mPercentCovered"
  30:                 column="PercentCovered"
  31:                 access="field"
  32:       />
  33:       <property name="mProjectTitle"
  34:                 type="String"
  35:                 access="field"
  36:                 formula="( SELECT prj.Title 
  37:                            FROM Projects prj  
  38:                            WHERE prj.ProjectID = ProjectID)"
  39:       />
  40:       <property name="mContact"
  41:                 type="String"
  42:                 access="field"
  43:                 formula="( SELECT prj.Contact 
  44:                            FROM Projects prj  
  45:                            WHERE prj.ProjectID = ProjectID)"
  46:       />
  47:       <property name="mRequiredCertification"
  48:                 type="String"
  49:                 access="field"
  50:                 formula="( SELECT CASE WHEN CertificationRequired = 1 THEN cert.Title ELSE 'None' END
  51:                            FROM TaskCertifications taskCert
  52:                            LEFT OUTER JOIN Certifications Cert
  53:                                ON taskCert.CertificationID = Cert.CertificationID
  54:                            WHERE taskCert.IsPrimary = 1 AND taskCert.TaskID = TaskID )" />
  55:        <property name="mResourceLanguage"
  56:                 type="String"
  57:                 access="field"
  58:                 formula="( SELECT Lang.Title
  59:                            FROM ProgrammingLanguages Lang
  60:                            WHERE Lang.LanguageID = ProgrammingLanguageID )" />
  61:    </class>
  62: </hibernate-mapping> 

In essence, this file provides a mapping specification for each data element in ProcessResourceNeed class.  The data for this class comes from several tables in the database. I will not go though this file point by point.  Some of the complexity in the joined columns will go away with the next version of NHibernate which supports a much simpler mechanism for specifying joins.

The final question is where to put these class/table mapping XML files.  Here are the options:

Embedded in the Same Assembly.  You can define the mapping file as an embedded resource in the same assembly as the mapped class.  This is slightly better than using annotations in that it isolates the mapping specification such that it could be moved at a later time.  But the "change the code; trigger Q/A" problem is still there.  If you use annotations or embed the mapping files in the same assembly, you must tell NHibernate where they can be found.  Line 8 in the configuration file tells NHibernate to look in the assembly for annotations and embedded resources.

Embedded in a Different Assembly.  You can define the mapping file as an embedded resource in a different assembly from the assembly that holds the mapped class.  This gives you better flexibility in that it is possible to swap out the other assembly without code changes.  A change to the configuration file changes everything.  You could have several different mapping assemblies, each of which has gone through the Q/A cycle.  If there are compliance/accountability issues, this seems like a good choice.  It does limit the number of possible choices to a well-defined set of well-tested options.  Line 8 with a different target assembly could be used here.

A variation on this approach is to define all of the possible options in a single assembly and point out the specific mapping (embedded resource) file in the configuration.  Line 11 in the configuration file is an example of doing this.  This provides a bit more control albeit at the additional (very minor) cost of calling out each mapping individually. 

A Naked Mapping File.  Finally, it is possible to specify the mapping is a straight (non-embedded resource) XML file.  This provides the maximum flexibility, even if it gives the willies to the Q/A folks.  For example, one might write some precursor logic that determines the environment of the application at start up (that is, disconnected, minimally connected [think, dial-up], full connected but from outside of the firewall, and safely within the firewall) and modifies the mapping files accordingly.  [And, yes, the Q/A folks are right to have the willies.]  In any case, you have the power to make any changes that you want without any code changes.

A Final Note.  While it is nice to have choices, remember that just because you can do it, does not mean that you should do it.

Technorati tags: ,
New Comments to this post are disabled

About jons

Jon Stonecash is a technology consultant and has been designing, developing, and testing various kinds of software for such a long time that he has had the opportunity to make most of the serious software development mistakes at least once. His long term interests center about databases and the aspects of the application that handle data access and business logic. He is also interested in the tools that assist the development process, particularly code generation.