Thursday, November 5, 2009

NHibernate Tutorial

Note: This post is based on NHibernate version 2.1.

This is a tutorial to get you started with NHibernate.

At the end of this tutorial you will have:

  • A class library project containing one entity
  • A NHibernate configuration file
  • A NHibernate mapping file to map your entities
  • A database running on SQL Server 2005 (changing to another version of SQL server is very simple)
  • A NUnit test project

1 - Create a new class library project named PocoLib in a new solution. This project will contain your entity classes.

2 - In the same solution, create a new class library project called PocoLibTests

3 - In the PocoLibTests project add references to

  • PocoLib project
  • nunit.framework.dll
  • nhibernate.dll
  • LinFu.DynamicProxy
  • NHibernate.ByteCode.LinFu

Note: You do not have to add any references to nhibernate dlls in the PocoLib project. Your entity classes will not have any knowledge of NHibernate; they are real POCO. If you eventually use your entities in a Web app or Web Services project (most likely), it will be that app that will need those same references (except for Nunit of course).

4 - In PocoLibTests, rename class1.cs to CustomerCRUDTests.cs to your project.

using NHibernate;
using NHibernate.Cfg;
using NUnit.Framework;
using PocoLib;

namespace PocoLibTests
{
    [TestFixture]
    public class CustomerCRUDTests
    {

        [Test]
        public void Insert()
        {
            Customer c = new Customer()
                { FirstName = "Bubba", LastName = "Stuart" };

            c.Address.Country = "USA";
            c.Address.State = "Florida";

            using (ISession session = _factory.OpenSession())
            {
                session.SaveOrUpdate(c);
                session.Flush();
            }
        }
    }
}

This test does not compile for now but that gives you an idea of what we are going to work with.

5 - In the PocoLib project rename class1.cs to Address.cs:

namespace PocoLib
{
    public class Address
    {
        private string _zipCode;
        private string _country;
        private string _state;
        private string _street;

        public string Street
        {
            get { return _street; }
            set { _street = value; }
        }

        public string State
        {
            get { return _state; }
            set { _state = value; }
        }

        public string Country
        {
            get { return _country; }
            set { _country = value; }
        }

        public string ZipCode
        {
            get { return _zipCode; }
            set { _zipCode = value; }
        }
    }
}

6 - In the PocoLib project add a new class named Customer.cs:

using System;
using System.Collections.Generic;

namespace PocoLib
{
    public class Customer
    {
        private int _iD;
        private DateTime? _birthDate;
        private string _lastName;
        private string _firstName;
        private Address _address = new Address();

        public int ID
        {
            get { return _iD; }
            set { _iD = value; }
        }

        public string FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }

        public string LastName
        {
            get { return _lastName; }
            set { _lastName = value; }
        }

        public DateTime? BirthDate
        {
            get { return _birthDate; }
            set { _birthDate = value; }
        }

        public Address Address
        {
            get { return _address; }
            set { _address = value; }
        }

    }
}

As you can see, the model is simple: a customer has a few properties and an Address.

Note: At this point, the test project still does not compile because the _factory is not defined yet. We will define it really soon.

7 - Create a new database with a Customer table

CREATE DATABASE PocoLib
GO

USE PocoLib
GO

CREATE TABLE [dbo].[Customer](
[CustomerID] [int] IDENTITY(1,1) NOT NULL,
[FirstName] [varchar](50) NULL,
[LastName] [varchar](50) NOT NULL,
[BirthDate] [datetime] NULL,
[Street] [varchar](50) NULL,
[State] [varchar](50) NULL,
[Country] [varchar](50) NULL,
[ZipCode] [varchar](50) NULL)
GO

Notice how we are using two classes (Customer and Address) but a single Customer table. It is much cleaner to use two classes at the class design level. But since it is a one-to-one relationship, saving the address in the same table as customer makes total sense (it's simpler, faster and feel more natural).

8 - In the PocoLib project, add an xml file named PocoLib.cfg.xml. That will be your main NHibernate configuration file. It's arguable whether this file belongs to the PocoLib class library or to the host application (the test project in this case), but for now let's put it in PocoLib.

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
    <property name="connection.provider">
      NHibernate.Connection.DriverConnectionProvider
    </property>
    <property name="connection.driver_class">
      NHibernate.Driver.SqlClientDriver
    </property>
    <property name="connection.connection_string">
      Server=localhost;database=PocoLib;Integrated Security=SSPI;
    </property>
    <property name="dialect">
      NHibernate.Dialect.MsSql2005Dialect
    </property>
    <property name="show_sql">
      true
    </property>
    <property name='proxyfactory.factory_class'>
      NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu
    </property> 
  </session-factory>
</hibernate-configuration>

  • Change the connection string (line 10) if needed.
  • If you are not on SQL Server 2005, you can change the other settings. For instance, set the dialect setting to Nibernate.Dialect.MsSql2008Dialect if your on SQL Server 2008.

9 - In the PocoLib project, add an xml file named Customer.hbm.xml. That will be mapping file used to tell NHibernate where to persist and load our entities. (The same question about where this file should reside applies here too).

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="PocoLib"
                   namespace="PocoLib">

  <class name="Customer" lazy="false">
    <id name="ID" column="CustomerID">
      <generator class="native"/>
    </id>
    <property name="FirstName"/>
    <property name="LastName"/>
    <property name="BirthDate"/>
    <component name="Address">
      <property name="Street"/>
      <property name="State"/>
      <property name="Country"/>
      <property name="ZipCode"/>
    </component> 
  </class>
</hibernate-mapping>

I think the content of the file pretty much speaks for itself but here are a few things worth mentioning:

  • By default, NHibernate assumes that your database column names match your class property names.
  • The <id> element identifies the primary key column. The <generator class="native"> indicates that SQL Server will generate the primary key value; not NHibernate.
  • The Customer is defined as a <class> in the mapping. This is what NHibernate calls an entity.
  • The Address is declared inside the Customer as an<component> element. That basically means that the Address is a part of the Customer and it can't live without a customer. NHibernate components are also know as Value Object in Domain Driven Design.

10 - Copy the PocoLib.cfg.xml and Customer.hbm.xml to the test library \bin\debug\ folder.

11 - Add the missing _factory to the test class. Add this code at the top of the test class:

        private ISessionFactory _factory;

        [SetUp]
        public void Setup()
        {
            Configuration configuration = new Configuration();
            configuration.Configure("PocoLib.cfg.xml");
            configuration.AddXmlFile("Customer.hbm.xml");

            _factory = configuration.BuildSessionFactory();
        }

  • The first line create a Configuration object.
  • The second line loads the PocoLib.cfg.xml.
  • The 3rd line loads the mapping file for the Customer class.
  • The 4th line create a factory that we will use to create NHibernate Sessions (a connection)

13 - Run the Insert() test. Go look in the database and be amazed (or not). Notice how SQL statements are written to the output window (If you are using Test Driven at least). You can turn that on or off by changing the <property name="show_sql">true</property> in the .cfg.xml file.

12 - Here are the other CRUD tests. I'm sorry but I don't feel like writing all the Asserts; this is just a test project to experiment with the CRUD operations.

using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
using NUnit.Framework;
using PocoLib;

namespace PocoLibTests
{
    [TestFixture]
    public class CustomerCRUDTests
    {
        private ISessionFactory _factory;

        [SetUp]
        public void Setup()
        {
            Configuration configuration = new Configuration();
            configuration.Configure("PocoLib.cfg.xml");
            configuration.AddXmlFile("Customer.hbm.xml");

            _factory = configuration.BuildSessionFactory();
        }

        [Test]
        public void Insert()
        {
            Customer c = new Customer()
               { FirstName = "Bubba", LastName = "Stuart" };

            c.Address.Country = "USA";
            c.Address.State = "Florida";

            using (ISession session = _factory.OpenSession())
            {
                session.SaveOrUpdate(c);
                session.Flush();
            }
        }

        [Test]
        public void Update()
        {
            // Arrange : Create customer in order to update it
            Customer c = new Customer() 
                { FirstName = "Bubba", LastName = "Stuart" };
            using (ISession session = _factory.OpenSession())
            {
                session.SaveOrUpdate(c);
                session.Flush();
            }
            int id = c.ID;

            //Act : update
            using (ISession session = _factory.OpenSession())
            {
                c = session.Get<Customer>(id);
                c.FirstName = "James";
                session.SaveOrUpdate(c);
                session.Flush();
            }
        }

        [Test]
        public void Delete()
        {
            // Arrange : Create customer in order to delete it
            Customer c = new Customer()
                { FirstName = "Bubba", LastName = "Stuart" };
            using (ISession session = _factory.OpenSession())
            {
                session.SaveOrUpdate(c);
                session.Flush();
            }
            int id = c.ID;

            //Act : Delete
            using (ISession session = _factory.OpenSession())
            {
                c = session.Get<Customer>(id);
                session.Delete(c);
                session.Flush();
            }
        }

        [Test]
        public void Query()
        {
            // Arrange : Create a customer in order to query it
            Customer c = new Customer() 
                 { FirstName = "Bubba", LastName = "Stuart" };
            using (ISession session = _factory.OpenSession())
            {
                session.SaveOrUpdate(c);
                session.Flush();
            }

            IList<Customer> list = null;

            //Act:
            using (ISession session = _factory.OpenSession())
            {
                list = session.CreateQuery(
                   "from Customer where LastName is not null")
                  
.List<Customer>();
            }

            //Assert:
            Assert.AreNotEqual(0, list.Count);

        }

    }
}

I hope this helps.

BTW: I started learning NHibernate just a few days ago. I'm wrote this post to serialize what I have learned so far (and hopefully help people at the same time). I'm sure this approach is not ideal. If you have comments on how I could improve this tutorial, just let me know.

Thanks

3 comments:

  1. Thanks, got the link to your blog via stackoverflow.

    Code works perfect

    BTW with product.hbm.xml you can set the build action to 'content' and copy to output directory to 'copy always' to avoid having to copy the file manually.

    That also lead me to fixing the fault in nHibernate's getting started guide - the mapping XML was being compiled to the incorrect location :-)

    ReplyDelete
  2. nice blog
    having a problem in many to one mapping please help

    http://forums.asp.net/p/1612888/4126755.aspx#4126755

    waiting for ur reply

    ReplyDelete
  3. How to add a reference to LinFu.DynamicProxy and the other one? I dont find it anywhere on my system.

    -Sai

    ReplyDelete