Aug
13
2009
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.- Make a new Class Library project called NStackExample.Data
- In the new Data project, add references to your core project, NHibernate.dll and FluentNHibernate.dll
- Add a reference to System.Configuration.dll so we can easily retrieve some application settings later.
- Also, the web project needs a reference to the data project.
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.
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 ClassThe 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:
- Executing this script will drop and recreate every table associated with your model. That can be devastating in a production environment.
- 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.
blog comments powered by Disqus