How-To: Using the N* Stack, part 3

This is the third installment in my series. In part 1, we downloaded our libraries and set up our solution. In part 2, we built our model. In this part, we’ll configure NHibernate and set up our database mappings. We’ll also set up our database schema.

Java – A language of XML files loosely coupled by code.

Before we can talk about Fluent NHibernate, you need to know a little bit about setting up mappings in plain old NHibernate. In a typical NHibernate setup, you’ll have a bunch of mapping files like this:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
    <class name="NStackExample.Address, NStackExample.Core" table="Address">
        <composite-id>
            <key-many-to-one name="Person" class="NStackExample.Person, NStackExample.Core" column="ID" />
            <key-property name="Type" type="Int32" />
        </composite-id>
        <property name="City" type="String" length="255" />
        <property name="Lines" type="String" length="255" />
        <property name="State" type="String" length="2" />
        <property name="Zip" type="String" length="10" />
    </class>
</hibernate-mapping>
You’ll have one of those for each of your entities. It’s left over from Java’s Hibernate project, and in my opinion, It’s a royal pain, complete with ruby scepter. Lucky for you, there’s a better way™.

A Better Way™: Fluent Mappings

With Fluent NHibernate, the mapping file above can be expressed using this class instead:
Imports FluentNHibernate.Mapping

Public Class AddressMapping
    Inherits ClassMap(Of Address)

    Public Sub New()
        UseCompositeId _
            .WithKeyReference(Function(x As Address) x.Person) _
            .WithKeyProperty(Function(x As Address) x.Type)
        Map(Function(x As Address) x.Lines).WithLengthOf(255)
        Map(Function(x As Address) x.City).WithLengthOf(255)
        Map(Function(x As Address) x.State).WithLengthOf(2)
        Map(Function(x As Address) x.Zip).WithLengthOf(5)

    End Sub

End Class
using FluentNHibernate.Mapping;

namespace NStackExample.Data
{

    public class AddressMapping : ClassMap<Address>
    {

        public AddressMapping()
        {
            UseCompositeId()
                .WithKeyReference(x => x.Person)
                .WithKeyProperty(x => x.Type);
            Map(x => x.Lines).WithLengthOf(255);
            Map(x => x.City).WithLengthOf(255);
            Map(x => x.State).WithLengthOf(2);
            Map(x => x.Zip).WithLengthOf(5);
        }

    }
}
It may look even more complicated than the XML mapping, but with Intellisense, it’s a breeze. Plus, there are no magic strings to worry about. When you change a property name using a refactor tool, your mapping won’t be left out of sync. Now that you have the basic idea, let’s get back on track.

Where?

Since the database connection, NHibernate configuration, entity mappings, and DAO implementations are really just implementation details of our chosen ORM, they should go in a separate assembly.
  1. Make a new Class Library project called NStackExample.Data
  2. In the new Data project, add references to your core project, NHibernate.dll and FluentNHibernate.dll
  3. Add a reference to System.Configuration.dll so we can easily retrieve some application settings later.
  4. Also, the web project needs a reference to the data project.
Now, let’s make our mappings.
Imports FluentNHibernate.Mapping

Public Class CourseMapping
    Inherits ClassMap(Of Course)

    Public Sub New()
        Id(Function(x As Course) x.ID).GeneratedBy.GuidComb()
        Map(Function(x As Course) x.Subject).Not.Nullable.WithLengthOf(4).UniqueKey("CourseNaturalKey")
        Map(Function(x As Course) x.CourseNumber).Not.Nullable.WithLengthOf(4).UniqueKey("CourseNaturalKey")
        Map(Function(x As Course) x.Title).Not.Nullable.WithLengthOf(255)
        Map(Function(x As Course) x.Description).Not.Nullable.WithLengthOf(1024)
        Map(Function(x As Course) x.Hours).Not.Nullable()

        HasMany(Function(x As Course) x.Sections) _
            .AsSet() _
            .WithForeignKeyConstraintName("CourseSections")

    End Sub

End Class
using NStackExample;
using FluentNHibernate.Mapping;

namespace NStackExample.Data
{
    public class CourseMapping : ClassMap<Course>
    {
        public CourseMapping()
        {
            Id(x => x.ID).GeneratedBy.GuidComb();
            Map(x => x.CourseNumber)
                .Not.Nullable()
                .WithLengthOf(4)
                .UniqueKey("CourseNaturalKey");

            Map(x => x.Subject)
                .Not.Nullable()
                .WithLengthOf(4)
                .UniqueKey("CourseNaturalKey");

            Map(x => x.Title)
                .Not.Nullable()
                .WithLengthOf(255);

            Map(x => x.Description)
                .Not.Nullable()
                .WithLengthOf(1024);

            Map(x => x.Hours)
                .Not.Nullable();

            HasMany(x => x.Sections)
                .AsSet()
                .WithForeignKeyConstraintName("CourseSections");

        }

    }
}
Most of this is self-explanatory and works exactly like you would expect. Our mapping class inherits from ClassMap(Of Course). ClassMap is the specific type that Fluent NHibernate searches for when looking for mappings. In this case, it signifies that this class provides the mapping for our Course entity. In the constructor, we define our specific mapping for each property.
  • Id sets up the persistent object identifier (POID). This is basically the primary key for the table. If you have more than one property in the primary key, as in the case of natural keys, go with UseCompositeId like in the address example above. Using multi-part keys isn’t really suggested and to my knowledge, isn’t fully supported by Fluent NHibernate.
  • GeneratedBy specifies the POID generator. How will you assign your keys? In my case, I use GuidComb. I get all of the benefits of guid identifiers, but I don’t fragment my database index nearly as much. You can read up on it more in Davy Brion‘s post on the NHForge blog.
  • Map simply maps a property to a database column. You can specify Not.Nullable and WithLengthOf as necessary.
  • UniqueKey specifies a unique index on the column. If you specify the same name on several columns, all of those columns will be part of the same unique index. In this example, we are forcing our natural key to be unique. Each combination of subject and course number must be unique. There can only be one ENGL 1301 course. Thank goodness.
  • HasMany defines a one-to-many relationship. You can specify the exact behavior of the collection. You have several options here, but the two types I use almost exclusively are Set and Bag.
    • AsSet doesn’t allow duplicate items.
    • With AsBag, duplicates are allowed.
By default, all relationships are lazy-loaded. This means that when you fetch a course from the database, the associated sections aren’t fetched right away. It works just like you would expect: They aren’t fetched until you access the Sections property. If you never access the Sections property, those sections are never fetched from the database, which can greatly improve performance. This is all made possible with proxies, but that’s another series of posts. Now let’s map the sections:
Imports FluentNHibernate.Mapping

Public Class SectionMapping
    Inherits ClassMap(Of Section)

    Public Sub New()
        Id(Function(x As Section) x.ID).GeneratedBy.GuidComb()

        Map(Function(x As Section) x.FacultyName).WithLengthOf(255)
        Map(Function(x As Section) x.RoomNumber).WithLengthOf(10)
        Map(Function(x As Section) x.SectionNumber) _
            .WithLengthOf(4) _
            .Not.Nullable() _
            .UniqueKey("SectionNaturalKey")

        References(Function(x As Section) x.Course) _
            .Not.Nullable() _
            .UniqueKey("SectionNaturalKey")

        References(Function(x As Section) x.Term) _
            .Not.Nullable() _
            .UniqueKey("SectionNaturalKey")

        HasMany(Function(x As Section) x.StudentSections) _
            .AsSet() _
            .WithForeignKeyConstraintName("SectionStudentSections")

    End Sub
End Class
The References function maps the Many-to-one relationship. Think of it as the other side of our one-to-many relationship. It is the reference from the child – section - back to it’s parent - course. For homework, finish mapping all of the entities. I bet you’re thinking this post is getting long considering we haven’t even started building the database. Well don’t worry. NHibernate will do that for us.

8 hours or 8 minutes?

Before I discovered NHibernate, I would spend at least a day setting up my database. It was insane. It drove me insane. I bet it drives you insane. It ends today. Disclaimer: If you are trying to use an existing shared legacy database, the chances of your existing DB schema working without some tweaking are slim. This post by Fabio Maulo explains your options. First, let’s configure NHibernate. The Fluent NHibernate Wiki has a great page explaining the fluent configuration of NHibernate.
Imports NHibernate
Imports NHibernate.Tool.hbm2ddl
Imports FluentNHibernate.Cfg
Imports System.Configuration
Imports System.IO

Public Class Configuration

    Private m_SchemaPath As String
    Private m_Factory As ISessionFactory

    Public Function Configure() As Configuration
        m_SchemaPath = ConfigurationManager.AppSettings("NStackExample.Data.Configuration.SchemaPath")
        m_Factory = Fluently.Configure _
            .Database(Db.MsSqlConfiguration.MsSql2005 _
                      .ConnectionString(Function(x As Db.MsSqlConnectionStringBuilder) _
                                            x.FromConnectionStringWithKey("NStackExample.Data.Configuration.DB"))) _
            .Mappings(Function(x As MappingConfiguration) _
                          x.FluentMappings.AddFromAssemblyOf(Of CourseMapping)() _
                          .ExportTo(m_SchemaPath)) _
            .ExposeConfiguration(AddressOf BuildSchema) _
            .BuildSessionFactory()
        Return Me
    End Function

    Private Sub BuildSchema(ByVal Cfg As NHibernate.Cfg.Configuration)
        Dim SchemaExporter As New NHibernate.Tool.hbm2ddl.SchemaExport(Cfg)
        SchemaExporter.SetOutputFile(Path.Combine(m_SchemaPath, "schema.sql"))
        SchemaExporter.Create(False, True)
    End Sub

    Public Function OpenSession() As ISession
        If m_Factory Is Nothing Then Configure()
        Return m_Factory.OpenSession
    End Function

End Class
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using System.IO;
using System.Configuration;

namespace NStackExample.Data
{
    public class Configuration
    {

        private ISessionFactory m_Factory;
        private string m_SchemaPath;

        public Configuration Configure()
        {

            m_SchemaPath = ConfigurationManager.AppSettings["NStackExample.Data.Configuration.SchemaPath"];

            m_Factory = Fluently.Configure()
                .Database(MsSqlConfiguration.MsSql2005
                        .ConnectionString(
                         x => x.FromConnectionStringWithKey("NStackExample.Data.Configuration.Db")))
                .Mappings(x => x.FluentMappings.AddFromAssemblyOf<CourseMapping>()
                                .ExportTo(m_SchemaPath))
                .ExposeConfiguration(BuildSchema)
                .BuildSessionFactory();

            return this;
        }

        private void BuildSchema(NHibernate.Cfg.Configuration cfg)
        {
            SchemaExport SchemaExporter = new SchemaExport(cfg);
            SchemaExporter.SetOutputFile(Path.Combine(m_SchemaPath, "schema.sql"));
            SchemaExporter.Create(true, false);
        }

        public ISession OpenSession()
        {
            if (m_Factory == null) Configure();
            return m_Factory.OpenSession();
        }

    }
}
The configuration falls in to two sections: Database and Mappings. In our case, the database is SQL 2005 and the connection string is read from a connection string element in the web.config. All of the mappings are fluent, not auto-mapped. Notice that we are exporting our mappings to a directory specified in the appsettings section of the web.config. This will convert our fluent mappings to individual hbm.xml files. This is great for debugging the mappings, especially when asking for NHibernate help online. We have one additional item. We’re using the ExposeConfiguration method to call our BuildSchema function, passing in our complete NHibernate configuration. In BuildSchema, we use a great hidden tool in NHibernate: the schema export. This amazing class will build your database for you. The create function takes two boolean parameters. The first specifies if the schema should be written out to a ddl file – a database script to build all of the tables, keys, indexes, and relationships in your database. The second boolean parameter specifies if the script should be executed against the specified database. It’s that easy. Two warnings:
  1. Executing this script will drop and recreate every table associated with your model. That can be devastating in a production environment.
  2. The script doesn’t start with a a “use [databasename]” statement, so if you’re not careful, when you execute it, you’ll build everything in the master database.
One last note: As with any project, you will have to adapt as you build. These mappings are not exactly what we use in the final build. I can guarantee our model will change significantly. I will take you through those changes as they happen, and explain the reasons behind them. I’ve decided not to post the complete source code at this stage. Instead, I leave the remaining mappings as an exercise for you, the reader. They will be included in the next source release. In the next post, I’ll show you how to test your mappings – including querying, reading from and writing to the database. Jason - Mapped out. Good night. P.S. – Special thanks to Tuna, Fabio, and Oren for the feedback, answers to stupid questions, and great advice!
blog comments powered by Disqus