Introduction
This article grew out of my frustration trying to get started with NHibernate
. It seemed that all the introductory material I found was either very vague or so detailed that I got overwhelmed before getting to first base. What I was looking for was a simple, straightforward tutorial that would get me up to speed on the fundamentals of NHibernate
as quickly as possible. I never found it. Hopefully, this article will serve those needs for other people.
This article is going to be rather lengthy, but I encourage you to work your way through it. NHibernate
is a complex piece of software, with a steep learning curve. This article will flatten the curve from a matter of days or weeks to a matter of a few hours.
The Problem
NHibernate
addresses the well-known problem that object persistence code is a pain in the neck to develop. Various articles estimate that from one-quarter to one-third of application code in an n-tier application is dedicated to the "persistence tier"—reading business object data from a database and writing it back again. The code is repetitive, time-consuming, and a chore to write.
Various solutions to this problem are available. Code generators can create data access code in seconds. But if the business model changes, the code has to be re-generated. "Object-relational managers" (ORMs) like NHibernate
take a different approach. They manage data access transparently, exposing a relatively simple API that can load or save an entire object graph with a line or two of code.
Introducing NHibernate
Hibernate
is a persistence engine in the form of a Framework. It loads business objects from a database and saves changes from those objects back to the database. As we mentioned above, it can load or save an entire object graph with just a line or two of code.
NHibernate
uses mapping files to guide its translation from the database to business objects and back again. As an alternative, you can use attributes on classes and properties, instead of mapping files. To keep things as simple as possible, we're going to use mapping files in this article, rather than attributes. In addition, mapping files make for a cleaner separation between business logic and persistence code.
So, one need only add a few lines of code to an application and create a simple mapping file for each persistent class, and NHibernate
takes care of all database operations. It is amazing how much development time is saved by using NHibernate
.
Note that NHibernate
is not the only ORM framework in the .NET universe. There are literally dozens of commercial and open source products that provide the same services. NHibernate
is among the most popular, probably because of its heritage as a descendant of Hibernate
, a popular ORM Framework in the Java universe. In addition, Microsoft has promised an 'Entity Framework' for ADO.NET, to provide ORM services. However, the product has been delayed, and it may not be released for some time.
Installing NHibernate
The first step in using NHibernate
is to download NHibernate
and Log4Net
, an open-source logging application that NHibernate
can use to record errors and warnings. NHibernate
contains the most recent Log4Net
binary, or you can download the entire Log4Net
install package. Here are the download locations:
Log4Net
is not strictly required to use NHibernate
, but its automatic logging can be very useful when debugging.
Getting Started
In this article, I am going to use a very simple demo application (the demo app) that does no real work, other than demonstrating data access with NHibernate
. It is a console application, which simplifies things by eliminating UI code. The application creates some business objects, uses NHibernate
to persist them, and then reads them back from the database.
You will need to do several things to run the demo app on your machine:
- Replace references to
NHibernate
and Log4Net
- Attach the NhibernateSimpleDemo database
- Modify the connection string
The demo app contains references to NHibernate
and Log4Net
. The references should be valid on your PC, so long as NHibernate
and Log4Net
are installed in their default locations. If the references aren't valid, you will need to replace them with references to NHibernate
(NHibernate.dll) and Log4Net
(log4net.dll) as they are installed on your PC. The DLLs can be found in the NHibernate installation folder on your development PC.
The demo app is configured to use SQL Server Express 2005. The database files (NhibernateSimpleDemo.mdf and NhibernateSimpleDemo.ldf) are packaged with the demo app. You will need to attach the database to SQL Server on your machine.
Finally, the connection string in the App.config file assumes that you are running a named instance of SQL Server Express 2005, and that the instance is named 'SQLEXPRESS'. If your PC is running a different configuration of SQL Server 2005, you will need to modify the connection string in the App.config file. Note that the database will not work with older versions of SQL Server.
The Business Model
There are two ways to develop an application with NHibernate
. The first is a "data-centric" approach, which starts with a data model and creates business objects from the database. The second is an "object-centric" approach, which starts with a business model and creates a database to persist the model. The demo app uses the object-centric approach.
Here is the business model for the demo app:
The model represents the skeleton of an order system. The model is not complete—there are just enough classes to demonstrate object persistence with NHibernate
. And there is a minimum of detail within each class. And it should be obvious that the design of the model doesn't represent best practice. But it's enough to show how NHibernate
works.
This article will use the model to demonstrate several aspects of object persistence with NHibernate
:
- Persisting simple properties;
- Persisting 'components' (objects with no corresponding database table);
- Persisting one-to-many associations; and
- Persisting many-to-one associations; and
- Persisting many-to-many associations
This article won't deal with more advanced topics, like inheritance. There is a wealth of technical information about NHibernate
available on the web. This article is designed simply to get you up and running.
The model is made up of five classes, four of which are persistent. The non-persistent OrderSystem
class serves as the root of the object model. We instantiate an OrderSystem
object when we initialize the application. Then we load the other objects into the OrderSystem
.
The OrderSystem.Customers
property holds a seller's Customer
list. Customer
s can be accessed by their CustomerID
. Each Customer
object holds the ID, name and address of a customer
, and a list of orders placed by the customer
. The address is encapsulated in a separate Address
class.
The Order
class contains the order ID of an order, its date, a reference to the customer placing the order, and a collection of the products in the order. The Product
class holds only the ID and name of a product—remember, we are only trying to show how NHibernate
works. Product objects are created when the application is initialized and loaded into the OrderSystem.Catalog
property. When an Order
object is created, Product
object references are copied from the OrderSystem.Catalog
property and added to the Order.OrderItems
property.
One of NHibernate
's strongest features is that it doesn't require special interfaces on business classes. In fact, business objects are generally not aware of the persistence mechanism used to load and save them. The mapping date that NHibernate
uses is contained in separate XML files.
This approach loosens the coupling between business classes and data-access classes, resulting in a more flexible, easier-to-maintain business tier. The only requirement that NHibernate
imposes is that collections be typed to interfaces, rather than concrete types. That's a practice that is generally recommended for all OO programming, and it does not bind business classes to NHibernate
in any way.
The Database
Here is the database that the demo app uses to persist the model:
Note that the database and the object model do not match perfectly. The object model has an Address
class that has no corresponding table in the database, and the database has an OrderItems
table that has no corresponding class. This mismatch is intentional. One of the aspects of NHibernate
that we want to show is that there need not be a one-to-one correspondence between classes and database tables.
Here are the reasons for the mismatch:
- The
Address
class does not represent an entity in the business model. Instead, it represents a value held in an entity, in this case, the Customer.Address
property. We encapsulated the address in a separate class so that we can demonstrate what NHibernate
calls "component mapping". - The
OrderItems
table is a link table in a many-to-many relationship between Order
s and Product
s. As such, it does not represent an entity from the business model.
The Customer
s table contains a skeleton of the usual customer
information, including the customer
's address. Best practice would call for the address
in a separate table, contrary to what we have done here. We included address
information in the Customer
s table so we could demonstrate how to persist what NHibernate
calls 'components'—classes that do not have their own tables. We will discuss components in more detail below.
The Order
s table has a bare minimum of information; only the ID (order number), Date, and CustomerID of the customer placing the order. The data relation between Order
s and Customer
s is maintained by a foreign key from the Orders.CustomerID
column to the Customers.ID
column.
Order
items require a many-to-many relationship (each order
can contain many items, and each product
can appear in many order
s), so we use the OrderItems
table as an intermediary. It simply links an order
number to a product
ID.
Again, the database is not intended as a best practice, or even real world, design. It contains just enough information to show how NHibernate
works.
Mapping the Business Model
Many introductions to NHibernate
start with configuration code, but we are going to start at a different place: mapping classes. Mapping is the heart of what NHibernate
does, and it presents the greatest stumbling blocks for beginners. Once we have discussed mapping, we will turn to the code required to configure and use NHibernate
.
Mapping simply specifies which tables in the database go with which classes in the business model. Note that we will refer to the table to which a particular class is mapped as the "mapping table" for that class.
We noted above that NHibernate
does not require any special interfaces or other code in a class that is to be mapped. It does, however, require that declared as virtual, so that it can create proxies as needed. The NHibernate
documentation discusses this requirement. For now, simply note that all properties in all of the business model classes in the demo app are declared as virtual.
Mapping can be done by separate XML files, or by attributes on classes, properties, and member variables. If files are used for mapping they can be incorporated in the project in any of several ways. To keep things simple, we are going to show one way of mapping in this article: We will map to XML files that are compiled as resources of an assembly.
You can map as many classes as you want in a mapping file, but it is conventional to create a separate mapping file for each class. This practice keeps the mapping files short and easy to read.
To begin our examination of mapping, let's take a look at the mapping file Customer.hbm.xml. The hbm.xml extension is the standard extension for NHibernate
mapping files. We have placed the files in the Model folder, but we could have placed them anywhere in the project. What's important is that the BuildAction
property of the file be set to Embedded Resource. This setting will cause the mapping file to be compiled into the assembly, so that it can't get lost or separated from the application.
The opening tags of any mapping file are standard:
="1.0"
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="NHibernateSimpleDemo"
assembly="NHibernateSimpleDemo">
The first tag is an XML declaration, and the second tag defines the XML namespace. You can include XSD information here, as well. The second tag also contains attributes that define the namespace and assembly names to be used with mapping references. This keeps us from having to include fully-qualified class names in the mapping tags.
The <class> Tag
The next tag identifies the class we are mapping in this file:
<class name="Customer" table="Customers" lazy="false">
The <class>
tag's attributes specifies the class being mapped, and its mapping table in the database:
- The name attribute specifies the class being mapped
- The table attribute specifies the mapping table for that class
- The lazy attribute tells
NHibernate
not to use 'lazy loading' for this class
'Lazy loading' tells NHibernate
not to load an object from the database until the application needs to access its data. That approach helps reduce the memory footprint of a business model, and it can improve performance. To keep things simple, we aren't going to use lazy loading in this application. However, you should learn its ins and outs as soon as possible after you get up and running with NHibernate
.
Note that there are a number of optional attributes for the <class>
tag that are documented in the NHibernate
help. To keep things simple, we won't go into them here.
The <id> Tag
Once we have identified the class being identified and its mapping table, we need to specify the identity property of the class and its corresponding identity column in the mapping table. Note that when we set up the database, we specified the CustomerID
field as the primary key of the database. In the column's IdentitySpecification
property, we specified that the column was the identity column, that it should initialize at 1
and increment by the same value:
So, what we need to do is this:
- Specify the identity property in the
Customer
class; - Specify the record identity column in the
Customer
s table; and - Tell
NHibernate
to let SQL Server set the value of the CustomerID
column in the Customer
s table.
Here is how we do it:
<id name="ID">
<column name=" CustomerID " />
<generator class="native" />
</id>
The identity specification is set by a combination of attributes and enclosed tags:
- The
<id>
tag's name attribute specifies the identity property in the Customer
class. In this case, it is the ID
property. - The
<column>
tag's name attribute specifies the record identity column in the Customer
s table. In this case, it's the CustomerID
column. - The
<generator>
tag's class attribute specifies that record identity values will be generated natively by SQL Server.
Simple Properties
Once we have mapped the identity property for the class, we can begin mapping other properties. The Customer
class has one simple property, Name
. We want to map it to the Name
column of the Customer
s table. Since the property and column names are the same, our mapping is very simple:
<property name="Name" />
Note that we could map the Name
property to a column with a different name (for example, CustomerName
). In that case, as with the <id>
tag, we would simply include an attribute in the <property>
tag specifying the name of the target column.
The property tag contains a number of optional attributes that can be used to specify the property type, the property length, whether null
s are allowed, and so on. However, NHibernate
can infer this information using .NET reflection, so we have omitted these tags in the demo app.
'Component' Mapping
NHibernate
uses the term 'component' to refer to a class that does not have a corresponding database table. It follows a distinction often made between "entity classes" and "value classes".
- An entity class is a class that represents an entity in a business model. In our model, the entity classes are
Customer
, Order
, and OrderItem
. These classes represent business objects. - The
Address
class does not represent a business object—it provides a way to encapsulate a value of the Customer
object. In NHibernate
's terminology, it is a "component" of the Customer
object.
Note that NHibernate
's use of 'component' is completely unrelated to the .NET use of the term. A component is simply a class that acts as a value class for an entity class, and which has no table of its own.
A component class (value class) is not mapped in its own file. Instead, it is mapped in the file belonging to its parent class, in this case, the Customer
class:
<component name="Address">
<property name="StreetAddress" />
<property name="City" />
<property name="State" />
<property name="Zip" />
</component>
The <component>
tag is a compound tag. It begins like a <property>
tag, with an attribute that specifies the name of the property being mapped. NHibernate
will use .NET reflection to determine the property type (that is, the component class). You can specify the name of the class in an optional 'class' attribute.
The <component>
tag is a compound tag—it encloses <property>
tags to map each property of the component class. As a result, the Customer.hmb.xml file maps two classes—the Customer
entity class, and the Address
value class.
Associations Generally
OO design is built upon the notion that classes in a business model are associated with each other in various ways:
- One-to-one: An object is associated with exactly one other object. For example, a
Husband
object associated with a single Wife
object. - One-to-many: This type of association is often referred to as "containment". For example, a
Customer
object may contain a collection of references to all the Order
objects that represent order
s placed by the Customer
. - Many-to-one: Many objects can refer to a single object. For example, many
Order
objects representing order
s placed by a particular Customer
can hold references to a single Customer
object. - Many-to-many: Many objects can refer to many other objects. For example, an
Order
object may contain a collection of references to Product
objects that represent product
s in an order, and a Product
object can be contained in many different Order
objects.
Associations can be unidirectional or bidirectional. For the purposes of this article, we will treat bi-directional associations to be simply two unidirectional associations that run in opposite directions. So, we will speak of associations running from an "owning class" (the class that 'owns' the association) to a "target class". Now, let's turn to the associations in the Customer
class, and how they are mapped in the Customer.hbm.xml file.
Collection Mapping: One-To-Many
Examine the Customer
class, and you will see an Orders
property, which contains a list of orders
placed by the Customer
. One of the first things to notice about the Orders
property is that it is not typed to the .NET List<T>
collection. Instead, it is typed to the IList<T>
interface:
private IList<Order> p_Orders = new List<Order>();
public IList<Order> Orders
{
get { return p_Orders; }
set { p_Orders = value; }
}
The property is "declared" to be of type IList<Orders>
, and the property variable is "instantiated" as a List<Order>
.
That's because NHibernate
requires that collections be typed to interfaces, rather than implementations. As we noted above, typing to interfaces, rather than concrete classes, is considered good programming practice, and it doesn't bind the business model in any way to NHibernate
. Typing to interfaces gives NHibernate
flexibility in loading collections and improves its efficiency.
NHibernate
provides several different tags that can be used to map collections. Since this collection is an IList<T>
, we will use a <bag>
tag to map the association:
<bag name="Orders" cascade="all-delete-orphan" lazy="false">
<key column="CustomerID" />
<one-to-many class="Order" />
</bag>
The tag contains a name attribute, which specifies the property we are mapping. It also contains a cascade attribute, which specifies the cascade style to be applied to this association. "Cascading" means simply that NHibernate
will load, save, and delete all child objects. The all-delete-orphan value indicates that NHibernate
should cascade all saves and deletes, and that it should delete any orphans that are left as a result of deletion.
Note that the cascade style must be specified in any association mapping, or NHibernate
will not cascade saves and deletes. As an alternative, the <class>
tag can specify a default cascade style for the entire class, using a default-cascade attribute. However, this attribute only provides for cascades on saves and updates, but not for deletes. For that reason, it is better to use the "cascade" attribute in each association mapping.
Note also that we have turned off lazy loading for this association. As we noted above, lazy loading is very useful when dealing with large, multi-level collections. To keep things simple, it is turned off in the demo app. But we definitely recommend that you use it in production applications.
The <bag>
tag encloses two other tags:
- The
<key>
tag's column attribute specifies the column in the mapping table for the target class that is used as the foreign key between the mapping table in this class and the mapping table for the target class. - The
<one-to-many>
tag specifies that there is a one-to-many relationship between the class being mapped and the class named in the class attribute. In this case, that's the Order
class—one Customer
can contain many Orders
. Note that the class attribute is required.
Note that there appears to be something missing: We specified the target class, but not its mapping table! The <key>
tag specifies a column, but not a table. So how does NHibernate
know which table to use? The answer is that, since we specified the target class, NHibernate
can look up its mapping table in the target class mapping file. So we do not need to specify the target class mapping table here.
And with that, we have finished mapping the Customer
class. We can close the Customer
mapping file and move on to the Order
class.
Collection Mapping: Many-To-One
Open the Order.hbm.xml file. By now, the contents should look pretty familiar to you. There are the usual <class>
and <property>
tags, and a <set>
tag for the one-to-many association between an order and its items. But there is another association in the Order
class, an association to the Customer
class, so that each order
knows the Customer
that placed the order
.
At first glance, this might appear to be a one-to-one association—one order
to one Customer
. But that wouldn't be correct, because many Order
objects can contain a reference to a single Customer
object. Even though a single Order
object is associated with a single Customer
object, at the class level, the association is many-to-one.
The <many-to-one>
tag maps this association. It is a simple tag, in that it does not enclose other tags. The reason for its simplicity is that, although the association is many-to-one, the 'many' objects are associated one at a time. It is almost as simple as a <property>
mapping.
<many-to-one name="Customer"
class="Customer"
column="CustomerID"
cascade="all" />
The attributes themselves are straightforward:
- The
name
attribute specifies the name of the property being mapped in the owning class. In this case, it's the Customer
property of the Order
class. - The
class
attribute specifies the target class. In this case, the target is the Customer
class. - The
column
attribute specifies the column in the mapping table for the owning class that is used as the foreign key to the target class. In this case, it's the CustomerID
column of the Orders
table, since we are mapping the Order class. - The
cascade
attribute specifies the cascading style for this association.
The class attribute is optional; NHibernate
can determine the class through .NET reflection. The column attribute can be omitted if the column in the mapping table has the same name as the class property being mapped. However, since that would rarely be the case, the attribute would normally be included.
Collection Mapping: Many-To-Many
The final mapping of interest in the Order.hbm.xml file is the mapping of the OrderItems
property. Note that the OrderItems
property is of type IList<Product>
. The OrderItems
property contains a collection of references to Product
objects.
Initially, this association looks like a one-to-many association, since one Order
is associated with many Products
. But that association would imply that each Product
can only be associated with a single Order
. Obviously, any Product
can appear in many Orders
, so what we have here is a many-to-many association.
In the database, we use the OrderItems
table as a link table between the Orders
table and the Products
table. The OrderItems
table contains the ID of the order involved, and the ID of the product involved. The approach is the conventional means of handling a many-to-many association.
Note that we are working with a unidirectional many-to-many association. It runs from the Order
class to the Product
class. Bidirectional associations are more complex, and we don't cover them in this article.
So, how do we map a many-to-many association? In much the same way as we map a one-to-many association. We use a <bag>
tag, which encloses a <many-to-many>
tag:
<bag name="Orders" table="OrderItems" cascade="none" lazy="false">
<key column ="OrderID" />
<many-to-many class="Product" column="ProductID" />
</bag>
Here is how the <bag>
tag specifies the association:
- The
<bag>
tag's name attribute specifies the property in the Customer
class that is being mapped. In this case, it is the OrderItems
property. - The
<bag>
tag's table attribute specifies the link table. In this case, it is the OrderItems
table. - The
<bag>
tag's cascade attribute specifies the cascading style to be applied to this association. - The
<bag>
tag's lazy attribute turns off lazy loading for this association. - The
<key>
tag's column attribute specifies the foreign key column in the link table that relates the link table to the mapping table for the current class. In this case, the OrderID
column of the OrderItems
table links it back to the Orders
table. - The
<many-to-many>
tag's class attribute specifies the target class for the many-to-many association. In this case, it is the Product
class. - The
<many-to-many>
tag's column attribute specifies the foreign key column in the link table that relates the link table to the mapping table for the target class. In this case, it is the ProductID
column in the link table, which relates the link table to the Products
table of the database.
As in the case of the one-to-many association, we don't need to specify the mapping table for the target class. Since we have specified the target class, NHibernate
can look up its mapping table from the target class mapping file.
Note that the <bag>
tag's cascade attribute is set to none
. That's because we don't want to delete products from the catalog when we delete an order. The use of cascade
attributes in this manner gives us fine-grained control over cascading in our class persistence. By setting the one-to-many relation in the Customer
mapping file to all-delete-orphan, we ensure that persistence operations (saves, updates, and deletes) cascade from Customers
to their Orders
. By setting the cascade
attribute in the many-to-many relationship in the Orders
mapping file to none
, we stop the cascading at that point. That keeps products from being deleted from the catalog when an order is deleted.
The only other mapping in the Order
class is a simple property mapping for the Date
property. We will not spend time on that here, so you can close the Order
mapping file.
We will not examine the Product.hbm.xml mapping file, because we have already covered everything that it contains. A good exercise for the reader is to open it and identity all of the items in the file.
Debugging Mapping Documents
Most debugging of mapping documents is done at run time. When an application configures NHibernate
, it attempts to compile the mapping documents that it can find. If NHibernate
runs into a problem, it will throw an exception of type NHibernate.MappingException. You can handle these exceptions, or you can let them stop execution. In the latter case, the exception and stack trace can be read from the Log4Net
log. The most common exception will look something like this:
Could not compile the mapping document:
NHibernateSimpleDemo.Model.Order.hbm.xml --->
NHibernate.PropertyNotFoundException: Could not find a getter for
property 'OrderItems' in class 'NHibernateSimpleDemo.Order'
Debugging follows the usual pattern—fix the bug, recompile, and re-execute. If your application is able to complete configuring NHibernate
, you will know it had no problems with your mapping documents. We discuss configuration below.
Note that if NHibernate
complains that one of your classes is unmapped, even though you have created a mapping file for that class, check the <class>
declaration in the mapping file, to make sure you entered the name of the class and its mapping table correctly. If those are correct, verify that you set the file's Build Action property to Embedded Resource.
Integrating NHibernate
There is no single 'right' way to integrate NHibernate
into your application. The author's personal preference is to follow general three-tier architecture, and to place NHibernate
configuration and processing code in a data tier. The demo app has a Persistence folder, which contains a PersistenceManager
class.
The PersistenceManager
contains generic methods to persist each of the entities in the business model. While a single class is sufficient for the demo app, it is probably not best practice for a production application. In a real-world app, you may want to split these methods out to several persistence classes.
The PersistenceManager
class configures NHibernate
and holds a global reference to a SessionFactory
object. A SessionFactory
creates Session
objects. Sessions
are the basic NHibernate
unit of work. A session represents a conversation between your application and NHibernate
.
You can think of them as being one level up in a hierarchy from a transaction. A session generally encompasses one transaction, but it can include several. Basically, you open a NHibernate
session, execute one or several transactions, close the session, and dispose it.
Sessions
are created by a SessionFactory
object. A SessionFactory
is resource intensive and has a relatively high initialization cost. Sessions
, on the other hand, use limited resources and impose little initialization cost. So, the general approach is to create a global SessionFactory
when the application is initialized, and use that SessionFactory
to create session objects as needed.
The demo project initializes a PersistenceManager
object as part of the application initialization. The PersistenceManager
configures a SessionFactory
, which becomes a member variable of the PersistenceManager
. The application can call PersistenceManager.SessionFactory
to create sessions as needed.
Configuring NHibernate – Configuration Data
There are two elements to configuring NHibernate
:
- Configuration data
- Configuration code
Configuration data can be placed either in a separate configuration file in the application root directory, or in the App.config file for the application. To keep things simple, the demo app places the data in the App.config file. It means one less file to get lost or separated from the rest of the application.
At the beginning of this article, we suggested downloading Log4Net
along with NHibernate
. Log4Net
has its own configuration data, which we will place in the App.config folder as well. The Log4Net
configuration data is straightforward, so we will not cover it here. Note that both NHibernate
and Log4Net
need tags in App.config's <configSections>
section.
NHibernate
's configuration data is reasonably clear. The data is used by NHibernate
to create a SessionFactory
. Here are the SessionFactory
properties that are set from the data:
- Connection provider: The
IConnectionProvider
that NHibernate
should use. The demo app uses the default provider. - Dialect: Which database dialect to use. The demo app specifies the SQL Server 2005 dialect.
- Connection driver: Which ADO.NET driver to use. The demo app specifies the SQL Server client driver.
- Connection string: The connection string to use in connecting with the database. The connection string is a standard ADO.NET connection string; you do not need to modify a valid connection string to use it with
NHibernate
.
Note that the connection string in the demo app is valid for the author's development environment. You will need to change the connection string to match your database setup.
Configuring NHibernate – Configuring Log4Net
You may recall that we imported a reference to Log4Net
into the demo app project when we set it up. The first step to configuring NHibernate
is to configure Log4Net
. Note that if you use Log4Net
(its use is optional), Log4Net
must be configured before NHibernate
, since NHibernate
will expect to see Log4Net
when it is initialized.
Log4Net
configuration is easy. First, make sure that Log4Net
is enabled in the App.config file:
<logger name="NHibernate">
<level value="DEBUG" />
</logger>
Next, add the following attribute to your code. The demo app adds it above the namespace declaration for the PersistenceManager
class:
[assembly: log4net.Config.XmlConfigurator(Watch=true)]
namespace NHibernateSimpleDemo
{
public class PersistenceManager : IDisposable
{
…
}
The final step to configuring Log4Net
is to call its Configure()
method. The demo app encapsulates the call in a ConfigureLog4Net()
method, which is called from the PersistenceManager
constructor:
private void ConfigureLog4Net()
{
log4net.Config.XmlConfigurator.Configure();
}
Configuring NHibernate – Configuration Code
The demo app configures NHibernate
when it initializes the PersistenceManager
. The PersistenceManager
does the configuration in a private
method, which is called from the PersistenceManager
constructor. The configuration code is straightforward:
private void ConfigureNHibernate()
{
Configuration cfg = new Configuration();
cfg.Configure();
Assembly thisAssembly = typeof(Customer).Assembly;
cfg.AddAssembly(thisAssembly);
m_SessionFactory = cfg.BuildSessionFactory();
}
First, we create an NHibernate Configuration
object. Then we pass it the class mappings from the mapping files. Note that the AddAssembly()
method requires that all mapping files be embedded in the project assembly. To do this, set the BuildAction
property of each mapping file to Embedded Resource.
Once we have passed mapping files to the Configuration
object, we simply need to tell it to create a SessionFactory
for us. NHibernate
will find and read the configuration data it needs; we do not need to specify whether the data is found in App.config or in a separate XML file, and we do not need to explicitly load the data.
BuildSessionFactory()
returns a SessionFactory
object, which the demo app passes to the Persistence Manager's SessionFactory
member variable. After that, the app can call the SessionFactory
whenever it needs a new session by simply calling that property on the global SessionFactory
object.
Note that if we were using multiple classes; say, one persistence class for each entity in the business model, we would have to pass a reference to the SessionFactory
object to each of these classes, so they could create sessions as they need them. Since all of the demo app's persistence methods are encapsulated in a single class (PersistenceManager
), they can simply call the SessionFactory
as a member variable.
Using NHibernate to Persist Classes
One of NHibernate
's strongest features is its ability to automatically cascade loads, saves, and deletes. For example, when we save an Customer
object, NHibernate
will automatically save the customer's Order
objects that have changed since they were last saved. In other words, when we save an object, we save its entire object graph. That feature dramatically simplifies our persistence code.
In fact, by using .NET 2.0 generics, the demo app is able to dispense with the usual persistence classes all together. It's persistence code is reduced to a few generic methods that we can incorporate directly into the PersistenceManager
! Obviously, the demo app is not a complete, real world application, and good design might call for a more fine-grained approach to the persistence tier. But the demo app effectively illustrates how NHibernate
radically simplifies persistence code.
The PersistenceManager
class contains methods to implement all basic CRUD (create retrieve, update, and delete) operations:
Save()
: This method saves a new or existing object to the database.RetrieveAll()
: This method retrieves all objects of a given type from the database.RetrieveEquals()
: This method retrieves all objects of a given type where a property of those objects equals a specified value. The method uses NHibernate
's QueryByCriteria
feature, which can be implemented in a variety of retrieval methods to retrieve 'like' string, or values within a specified range.Delete()
: This method has two overloads. The first deletes a single object passed into it. The second deletes a list of objects passed into it.
Most of the methods follow a common pattern:
- They wrap a new
NHibernate
session object in a using
statement. The using
statement ensures that the session is properly closed and disposed when the method is through with it, even if an exception is thrown. The method RetrieveAll<T>()
is an exception, which we discuss below. - The
Save()
and Delete()
methods further wrap an NHibernate
transaction in a using
statement, for the same reason. - The method calls a generic method of the
NHibernate
session object, in order to perform the work that needs to be done.
Note that the PersistenceManager
includes a Close()
method, and that it implements the IDisposible
interface. That means the PersistenceManager
must be closed and disposed when the application is finished with it, which the demo app does.
The CRUD methods in the PersistenceManager
are not intended to represent a complete implementation of object persistence. The CRUD methods are intended to show the basics of how persistence works in NHibernate
. The NHibernate
documentation contains complete information about the CRUD methods provided by the session object and how to implement those methods in your application.
The Payoff
At this point, you may be asking yourself, as I did, whether NHibernate
has such a complicated setup that it might be just as easy to write CRUD code by hand. I personally found NHibernate
's learning curve to be rather steep and slow going.
Well, here is where it all pays off. If you think about what we have done so far, it is really little more than creating some short mapping files, and adding a small amount of code to our application. Once you have learned the system, it is really not too bad. What you get in return is considerable.
Think for a moment how much hand-written code would be required to load the Customers
collection, along with each Customer
's Order
objects, and each Order
's Product
objects. Here is the code required to do that with NHibernate
:
IList<T> itemList = session.CreateCriteria(typeof(T)).List<T>();
That's right—one line of code, to load an entire object graph. And here is the code required to write the same object graph to the database:
foreach (Customer Customer in OrderSystem.Customers)
{
using (ISession session = m_SessionFactory.OpenSession())
{
using (session.BeginTransaction())
{
session.SaveOrUpdate(item);
session.Transaction.Commit();
}
}
}
The actual save operation requires two lines of code, assuming you want transactional support! So, once the setup is done, NHibernate
makes very short work of CRUD operations. Let's turn now to how the demo app implements these operations.
Running the Demo App
The demo app is a console application, so it really has no user interface. Instead, the Program
class takes the role of the UI. The Main()
method communicates with a Controller
class, which manipulates the model in accordance with the requests passed to the Controller
by the Program
class. In addition, the Program
class subscribes to the OrderSystem.Populate
event, which fires whenever the OrderSystem
is rebuilt or loaded.
This approach is an implementation of "Model-View-Controller" (MVC) architecture, which you can learn more about in another article. If you are unfamiliar with MVC architecture, you may find it helpful to step through the demo app from the top of the Program.Main()
method, to understand the flow of control.
The Program.Main()
method simply sends requests to the Controller
, and these requests provide a broad overview of the actions carried out by the demo app. Note that the application pauses after each major step, to give you a chance to examine the console before moving on.
The application's first task is to instantiate a Controller
, which is the only object with which it communicates directly. Note that when the Controller
is initialized, it creates two other objects:
- An
OrderSystem
object, which contains the business model for the demo app; and - A
PersistenceManager
, which contains all of the persistence tier logic for the application.
As we noted above, the PersistenceManager
is a very lightweight class, since it delegates most of its work to NHibernate
.
These objects are visible only to the Controller
; the Program
class has no knowledge of the PersistenceManager
, and very limited knowledge of the OrderSystem
. This keeps our UI loosely coupled to the rest of the application. As a result, it should be very easy to re-design the demo app as, say, a Windows Forms application. We would only have to design a GUI that passed the same requests to the Controller
we already have, and handle the OrderSystem.Populated
event.
Next, the application clears all data from the database. To fully demonstrate the workings of NHibernate
, we will start with a clean slate each time we run the application. The PersistenceManager.ClearDatabase()
method illustrates an important point--we can mix calls to NHibernate
with ADO.NET calls. In this case, we need to make several simple ADO.NET calls to clear the database. So, the ClearDatabase()
method borrows NHibernate
's connection and creates an ADO.NET command object to do the work.
Saving a Business Model
Once it has cleared the database, the application builds the business model in memory. The OrderSystem.Populate()
method does the work, and the OrderSystem
fires a Populated
event when it is done. This event notifies the rest of the application that the business model has either been rebuilt or loaded from the database. The Program class subscribes to this event and uses it to print a list of customers and orders whenever the business model is reloaded or rebuilt.
Once the model has been built, the application saves it. And it is here that NHibernate
really shines. The Save<T>()
method in the PersistenceManager
shows how simple persistence code can be with NHibernate
. We don't even have to think about the database. The Controller
simply tells the PersistenceManager
to save our objects:
private static void SaveBusinessObjects
(OrderSystem OrderSystem, PersistenceManager persistenceManager)
{
foreach (Product product in OrderSystem.Catalog)
{
persistenceManager.Save<Product>(product);
}
foreach (Customer Customer in OrderSystem.Customers)
{
persistenceManager.Save<Customer>(Customer);
}
}
Note that the SaveBusinessObjects()
method saves Products
and Customers
, but not Orders
. Since we have turned cascading on for the Customer.Orders
property, Customers
' orders
are saved automatically when Customers
are saved. So, there is no need to save the Order
objects in the OrderSystem.Orders
list.
Note also that NHibernate
takes care of creating records for the OrderItems
table in the database, even though we don't have any code that instructs it to do so. That is because the Order
mapping file specifies the OrderItems
table as the link table in the many-to-many association contained in the Order.OrderItems
property. And that is another example of how NHibernate
simplifies persistence code.
Deleting Business Objects
After saving the business model, the application clears it from RAM. We do this to set up the next demo, but it also illustrates an important point: Deleting a business object from RAM does not remove its data from the database. As we will see below, we have to explicitly instruct NHibernate
to remove an object from the database. If we simply delete the business object from RAM, NHibernate
can reload the business object from the database at any time. That is the subject of our next demonstration.
Loading Objects with NHibernate
Once the business model has been deleted from RAM, the application reloads it from the database. This step shows how to load persistent objects. The application uses the Controller.LoadBusinessObjects()
method to load objects. This method is as simple as the other methods in the Program
class. It simply calls the PersistenceManager.RetrieveAll<T>()
method, which delegates most of its work to NHibernate
.
The RetrieveAll<T>()
method in the demo app is actually the second version of the method. The original version of the method was very simple. It created an NHibernate
session object, which it wrapped in a using
statement. Then, it used NHibernate
's "Query By Criteria" feature to fetch all objects of a particular type from the database:
public IList<T> RetrieveAll<T>()
{
using (ISession session = m_SessionFactory.OpenSession())
{
ICriteria targetObjects = m_Session.CreateCriteria(typeof(T));
IList<T> itemList = targetObjects.List<T>();
return itemList;
}
}
The type of the object fetched is specified by the T
type parameter. The method created an ICriteria
object specifying this type and invoked the List<T>()
method of the criteria to return a list of objects meeting the criteria.
This led to a subtle problem that's easy to miss. The demo app loads Order
objects twice—once explicitly, and once implicitly:
- It loads
Order
objects explicitly when it loads the OrderSystem.Orders
list. - It loads
Order
objects implicitly when it loads the OrderSystem.Customers
list. The cascade
attribute in the <bag>
tag for the Orders
property, found in the Customer
mapping file, causes all of a customer
's orders
to be loaded automatically when the Custo<code>
mer object is loaded.
In other words, cascading applies to loads as well as saves. If we didn't have a separate Orders
collection, we could dispense with the explicit load. However, the OrderSystem.Orders
collection is handy to have around, since it lets us view orders without having to know the customer that placed them. So, we will keep the explicit load.
But loading Order
objects twice leads to a risk that NHibernate
creates two different Order
objects "that represent the same order", when what we want are two references to the "same object":
The issue is generally referred to as "object identity". If we end up with two different objects, then a change to an order in the Customers
list would not be reflected in the same order in the Orders
list! Obviously, the issue is very important.
Here is the rule: NHibernate
will guarantee object identity only if references are loaded during the same session. If you take another look at the old version of the RetrieveAll<T>()
method, you will see that it fails that test, since a different session is created for each type loaded. In short, the old version of the RetrieveAll<T>()
method produced duplicate Order
objects, instead of duplicate references to the same Order
object.
Here is how we fixed the method. First, we promoted the session variable to be a member variable, rather than a local variable. That change enables a session to last beyond the loading of a single type. Next, we added a new param to the method, SessionAction
. This param specifies what action the method should take with respect to the session member variable:
Begin
: The method should begin a new session.Continue
: The method should continue an existing session.End
: The method should continue an existing session, and end it when it is done.BeginAndEnd
: The method should begin a new session and end that session when it is done.
The revised method can load as many different types of objects as it needs to in the same session, which guarantees the identity of objects loaded more than once:
public IList<T> RetrieveAll<T>(SessionAction sessionAction)
{
if ((sessionAction == SessionAction.Begin) || (sessionAction ==
SessionAction.BeginAndEnd))
{
m_Session = m_SessionFactory.OpenSession();
}
ICriteria targetObjects = m_Session.CreateCriteria(typeof(T));
IList<T> itemList = targetObjects.List<T>();
if ((sessionAction == SessionAction.End) || (sessionAction ==
SessionAction.BeginAndEnd))
{
m_Session.Close();
m_Session.Dispose();
}
return itemList;
}
Here is how the method works:
- First, a new session is begun if one is needed.
- Then, the type is loaded as before, using query by criteria.
- Finally, the new session is closed as needed.
To keep things simple, the code omits the try-catch
block that you should use if you aren't wrapping code in a using
statement. Instead, if the method is told to end a session, the method simply closes and disposes the session.
As we noted above, the application calls the RetrieveAll<T>()
method only on the 'topmost' persistent objects; that is, Products
, Customers
, and Orders
. We do not have to explicitly retrieve OrderItems
, and we don't have to load Orders
into Customers
, or OrderItems
into Orders
. NHibernate
takes care of loading any child objects in an object graph.
Note that NHibernate
has several features for querying a database to load business objects:
- Query by criteria: Query the database by creating a
Criteria
object and setting its properties. - Query by example: Query the database by creating a sample object of the type you want to retrieve and setting its properties to specify selection criteria.
- Hibernate Query Language: A SQL-like query language.
- SQL statements: You can submit SQL statements to
NHibernate
if the other methods of querying a database do not fit your needs.
SQL statements should be used only as a last resort, if no other method of querying the database will work. The NHibernate
documentation discusses all of these options in detail.
Verifying Object Identity
Once the demo app has reloaded the business model, it demonstrates that object identity has been preserved. In fact, there are two objects that get loaded twice:
- As we discussed above,
Order
objects get loaded explicitly into the OrderSystem.Orders
property, and implicitly into the OrderSystem.Customers[i].Orders
property. - In addition,
Cus<code>
tomer objects get loaded explicitly into the OrderSystemCustomers
property, and implicitly into the OrderSystem.Orders[i].Customer
property.
The business model is set up so that the first Customer
(Able, Inc.) places the first order
. That fact allows us to test that:
- The first
Customer
in the Customers
list, and the Customer
in the first order
in the Orders
list are the same object. - The first
order
in the Orders
list, and the first order
for the first Customer
in the Customers
list, are the same object.
We verify object identity using the object.ReferenceEquals()
method, which tests whether two references point to the same object:
Customer CustomerA = OrderSystem.Customers[0];
Customer CustomerB = OrderSystem.Orders[0].Customer;
bool sameObject = object.ReferenceEquals(CustomerA, CustomerB);
…
Order orderA = OrderSystem.Orders[0];
Order orderB = OrderSystem.Customers[0].Orders[0];
sameObject = object.ReferenceEquals(CustomerA, CustomerB);
The demo app displays the results of its comparisons, then pauses for the user before moving on.
Removing Objects From the Database
As we saw above, deleting an object from the object model does not remove it from the database. The demo app demonstrated this point by clearing the object model, then reloading it from the database.
To remove an object from the database, we have to explicitly tell NHibernate
to do so. The demo app's PersistenceManager
contains a Delete<T>()
method that performs this task:
public void Delete<T>(IList<T> itemsToDelete)
{
using (ISession session = m_SessionFactory.OpenSession())
{
foreach (T item in itemsToDelete)
{
using (session.BeginTransaction())
{
session.Delete(item);
session.Transaction.Commit();
}
}
}
}
The method follows the same general pattern as the Save<T>()
method. Two using
statements are used to wrap an NHibernate
session and a transaction, respectively. The heavy lifting is done by a simple call to the session
's Delete()
method. A call to session.Delete()
will delete an object and (assuming cascading has been set in the mapping file) all of the object's children.
The demo app shows how this is done by removing the first Customer
, Able, Inc., from the database. Then it clears the object model, as it did before, and reloads the object model from the database. This time, there are only two Customers
, and only two Orders
. Not only did NHibernate
delete the Able, Inc. Customer record from the database, but it deleted Able, Inc.'s Orders
, and the OrderItems
in those orders, as well.
Putting The Focus Back Where It Belongs
We have completed our tour of basic CRUD operations with NHibernate
. I hope you come away with the following point: NHibernate
will dramatically simplify the persistence layer of your application. Once you have created mapping files for your classes, you can nearly forget about persistence. And you don't have to spend days or weeks coding a cumbersome persistence layer to get those benefits.
The persistence tier of the demo app illustrates how lightweight this tier can be when using NHibernate
. We have only a few methods, and most of those are generic, reducing the need for overloads or creating a persistence class for each business model class. The methods themselves are simple, and do not require much time to design or code. The amount of work that you have to do to persist your objects is slashed. Not bad for a free piece of software!
But there is another benefit, and it may be the most important. Take a look at the demo project in VS 2005's Solution Explorer, and you will see that most of the application's work is being done in the model. The Controller
simply manages the work and delegates tasks to the business model and the persistence manager, which is basically just a thin wrapper for NHibernate
. As a result, you are free to devote nearly all your attention to your business model. By relieving you of the chore of writing persistence code, NHibernate
enables you to keep your focus on your business model, where it belongs.
Where To Go From Here
We have really just scratched the surface of NHibernate
. Even so, you should have enough to get going. Play around with it on a couple of demo projects of your own, to get a feel for how it works. At that point you should be ready to dive into the documentation for the framework.
NHibernate
ships with two documentation files. The first is a general explanation of the Framework, and the second is a reference to NHibernate
's API. In addition, Manning Publications publishes NHibernate in Action (ISBN: 1-932394-92-3), which provides an extensive and detailed explanation of the Framework. It is available in eBook now and it is scheduled to appear in paperback in December 2007.
As you delve further into NHibernate
, be sure to learn about lazy loading and how it works. As we discussed above, lazy loading is essential to the efficient use of NHibernate
, and it is well worth the time to learn.
Conclusion
I hope you have enjoyed this introduction to NHibernate
, and that it will flatten the learning curve involved in getting up to speed with the Framework. Please post comments and questions on Code Project, and I will answer as many as I can. If you find any errors, please post a comment, so that I can correct it in a revised version of the article.