Jun 9, 2011

Getting started with NHibernate and ConfOrm (revamp)

I’m trying to post different things about NHibernate, but still post NHibernate for beginners is most popular. I wrote them in 2009 (Wow, how old I am! :D). First of all its in Russian and I think things have changed a bit since that time, mainly because of awesomeness of NuGet. This getting started will contain not only recommended mapping technique, but also references to some other useful things like GetHashCode, Equals implementations, transactions management, etc.

So lets get started. First of all install NuGet. Create a new empty MVC 3 project and add two class library projects name them %WhatYouLike%.Core and %WhatYouLike%.Data. First assembly will contain all business entities that NHibernate will save and load from data base. Data assembly is going to be used for NHibernate configuration, repositories, etc.

So when projects are created lets install NHibernate. In visual studio navigate to Tools –> Library Package Manager –> Package Manager Console. In the opened command prompt select your MVC project as default project and execute command: “Install-Package ConfOrm”. Do the same for Data project.

ConfOrm is an open source project that allows you to map all your domain model without any XML. I do use ConfOrm instead of Fluent NHibernate in this example just because first one is going to be added in core of NHibernate, so it is preferable (but of course not necessary) to get familiar with it.

Just can’t allow myself to forget about some political issues with ConfOrm and Fluent NHibernate :). ConfOrm project was started by Fabio Maulo who is lead of NHibernate project (correct me here if I’m wrong). And 3.2 version of NHibernate was released with mapping in code part that basically was very similar to ConfOrm way of doing things.

This fact has raised a little storm in twitter and James Gregory (author of Fluent NHibernate) wrote a blog post to explain what he thinks about it.

Some time later Fabio wrote a response post where he has shown how you can reuse your Fluent mappings with new API :).

So, don’t where it will end, but politics in open source space also takes place :). I recommend you to read mentioned two posts, just to be informed.

Now lets get back to our project. First thing we are going to do is add some entities to work with. NHibernate uses POCO objects. It means that you don’t need to inherit from base classes or implement some interfaces in order to create an entity that is going to be managed by NHibernate. Two things required:

  1. Entity should have public/protected default constructor (constructor with no parameters)
  2. All properties and methods should be virtual

So lets create some entities with associations and properties. Here is example domain model I’ve created:

domain model

Here is source code for one of the entities:

using System.Collections.Generic;

namespace nhrevamp.Core
{
    public class Post : Entity
    {
        public Post()
        {
            this.Comments = new HashSet<Comment>();
            this.Tags = new HashSet<Tag>();
        }

        public virtual string Title { get; set; }

        public virtual string Content { get; set; }

        public virtual ICollection<Comment> Comments { get; protected set; }

        public virtual ICollection<Tag> Tags { get; protected set; }
    }
}

All entities inherit from base class Entity with single property Id:

public abstract class Entity
{
    public virtual int Id { get; protected set; }
}

Notice that we are using ICollection from System.Collections.Generic not from Iesi.Collections. The reason for that is to keep our domain model as clean as possible. Also we initialize collections in constructor in order to avoid nasty NullReferenceException’s. And also we mark setters for collections as protected, just to avoid possibility to replace all collection with new one accidently.

So we have:

  1. One to many association (Post has many Comments)
  2. Many to one association (each Comment belongs to one Post)
  3. Many to many association (Post has many Tags and Tag has many Posts)

NHibernate has two main interfaces that you are going to work with. First is ISessionFactory. Instance of it should be used as a singleton and configured only once. This operation will take time, especially for complex domain models. The good news is that ISessionFactory is serializable and are able to cache it in order to not recreate it each time. Second interface is ISession. This one is used to query entities, update and delete them. Creation of ISession object is small, but you should consider some lifetime management issues with ISession. Here I’ve described how you can easily integrate it with MVC. 

In order to create ISessionFactory we need to configure it and provide mappings for our domain model. Lets start with configuration (you can read full list of available options here). Add new class to your .Data project, lets call it NHibernateConfigurator and create a single method there -BuildSessionFactory. Code there should be the following:

public class NHibernateConfigurator
{
    public ISessionFactory BuildSessionFactory()
    {
        var cfg = new Configuration();
        cfg.SessionFactory()
           .Proxy.Through<ProxyFactoryFactory>()
           .Integrate.Using<MsSql2008Dialect>()
           .Connected.ByAppConfing("connectionString");

        return cfg.BuildSessionFactory();
    }
}

So we say that we are going to work with SQL Server 2008 and connection string is placed in connection strings section of application settings file (web.config in our case).

This post is written for NHibernate 3.1 version. And this version has no default proxy factory with it. So you need to execute one more NuGet command for Data project: Install-Package NHibernate.LinFu. 3.2 version will be delivered with default proxy factory.

Now to the mappings. Its so easy, that you even wouldn’t believe:

public class DomainMapper
{
    public HbmMapping GenerateMappings()
    {
        IEnumerable<Type> domainEntities = GetDomainEntities();

        ObjectRelationalMapper relationalMapper = new ObjectRelationalMapper(); 
        relationalMapper.TablePerConcreteClass(domainEntities); // each concrete class should have its own table in DB
        relationalMapper.Patterns.PoidStrategies.Add(new NativePoidPattern()); // primary keys are generated by DB with identity field
        relationalMapper.Patterns.Sets.Add(new UseSetWhenGenericCollectionPattern()); // ICollection when met in classes should use Set in mappings
        relationalMapper.ManyToMany<Post, Tag>(); // Many to many association by some reasons cant be picked by ConfOrm. Need in set it manually
        relationalMapper.Cascade<Post, Tag>(Cascade.Persist); // when post is saved, tag also needs to be saved

        var patternsAppliers = new CoolPatternsAppliersHolder(relationalMapper); // this is set of column naming packs it used to get nice column names in FKs like PostId in Comments table
        patternsAppliers.Merge(new ClassPluralizedTableApplier(new EnglishInflector())); // means that Comment entity will have Comments table in DB
        Mapper mapper = new Mapper(relationalMapper, patternsAppliers);

        HbmMapping mapping = mapper.CompileMappingFor(domainEntities);
        File.WriteAllText(@"D:\mapping.xml", Serialize(mapping));

        return mapping;
    }

    private static IEnumerable<Type> GetDomainEntities()
    {
        Assembly domainAssembly = typeof(Entity).Assembly;
        IEnumerable<Type> domainEntities = from t in domainAssembly.GetTypes()
                                           where t.BaseType == typeof(Entity) && !t.IsGenericType
                                           select t;
        return domainEntities;
    }

    /// <summary>
    /// Generates XML string from <see cref="NHibernate"/> mappings. Used just to verify what was generated by ConfOrm to make sure everything is correct.
    /// </summary>
    protected static string Serialize(HbmMapping hbmElement)
    {
        var setting = new XmlWriterSettings { Indent = true };
        var serializer = new XmlSerializer(typeof(HbmMapping));
        using (var memStream = new MemoryStream())
        {
            using (var xmlWriter = XmlWriter.Create(memStream, setting))
            {
                serializer.Serialize(xmlWriter, hbmElement);
                memStream.Flush();
                byte[] streamContents = memStream.ToArray();

                string result = Encoding.UTF8.GetString(streamContents);
                return result;
            }
        }
    }
}

This code block contains one utility method called Serialize. It is used just to verify mappings. You can skip it. But its useful. That’s all. Some details I’ve added in comments. So now we are ready to start working with NHibernate.

Now we need provide generated mappings for our ISessionFactory. So add next two lines to the BuildSessionFactory method of NHibernateConfiguratorClass:

HbmMapping generateMappings = new DomainMapper().GenerateMappings();
cfg.AddDeserializedMapping(generateMappings, "domain");

Now we are ready to use NHibernate. First thing we need is actually DB schema. We can generate it from mappings with the help of SchemaExport class:

new SchemaExport(cfg).Execute(true, true, false);

This command is executed in NHibernateConfigurator class and creates all required tables for us:

Data base schema

In order to work properly NHibernate requires transactions to wrap all queries. I’ve described asp.net mvc integration in previous post. So I won’t describe it here.

The last thing you need to see here is a way to work with data. Here is how we can save new blog with post and some comments:

ISessionFactory sessionFactory = new NHibernateConfigurator().BuildSessionFactory();
using (ISession session = sessionFactory.OpenSession())
{
    using (ITransaction transaction = session.BeginTransaction())
    {
        Post post = new Post
                        {
                            Content = "test"
                        };
        post.Tags.Add(new Tag
                          {
                              Name = "test"
                          });
        post.Comments.Add(new Comment
                              {
                                  Author = "me",
                                  Content = "NH is awesome",
                                  Post = post
                              });
        session.SaveOrUpdate(post);
        transaction.Commit();
    }
}

Notice that we save only Post object, all associations are going to be saved by cascade. Generated SQL is straight forward:

begin transaction with isolation level: Unspecified

INSERT INTO Posts
           (Title, Content)
VALUES     (NULL /* @p0 */,
            'test' /* @p1 */)
select SCOPE_IDENTITY()

INSERT INTO Comments
           (Author, Content, PostId)
VALUES     ('me' /* @p0 */,
            'NH is awesome' /* @p1 */,
            1 /* @p2 */)
select SCOPE_IDENTITY()

INSERT INTO Tags
           (Name)
VALUES     ('test' /* @p0 */)

select SCOPE_IDENTITY()

INSERT INTO PostToTag
           (PostId,
            TagId)
VALUES     (1 /* @p0 */,
            1 /* @p1 */)
commit transaction

NHibernate has mature Querying API that was improved in 3.0 version. Also it has LINQ implemented with session.Query<T> extension method. This post has no intention do describe them.

So you are almost NHibernate Guru already ;). What to read next:

  • Overriding GetHashCode and Equals methods for your domain entities (link)
  • Primary keys generations and consequences of using them (link)
  • Lazy, eager loading (link) and select N+1 problem (link)
  • Transactions and Sessions lifetime management (link)
  • Inverse attribute for mapping collections (link)

Happy NHibernating!

No comments:

Post a Comment