Getting Rid Of Config Files with Fluent NHibernate
posted on 09/03/08 at 07:00:00 pm by Joel Ross
When I first looked at NHibernate, the biggest issue I had with it was the configuration files - they looked long, confusing and error-prone. Well, as evidenced by a previous post, I got over that, and did it anyway.
But just because I figured it out doesn't mean I was happy with it, or that it's still not error prone. Luckily, there's a solution: Fluent NHibernate. I'm not sure why it's called that, other than it has a fluent interface, but it's goal is to allow you to programmatically specify NHibernate configurations.
From my previous post, here was my config for my Team class:
1: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="CP.Models" namespace="CP.Models">
2: <class name="CP.Models.Team" table="Teams">
3: <id name="TeamId" column="TeamId" type="Int32" unsaved-value="0">
4: <generator class="native" />
5: </id>
6: <property name="Abbreviation" column="Abbreviation" type="string" length="50" not-null="false" />
7: <property name="Location" column="Location" type="string" length="50" not-null="false" />
8: <property name="NickName" column="NickName" type="string" length="50" not-null="false" />
9: </class>
10: </hibernate-mapping>
This is embedded in my DLLs, and it's just text. Hitting F5 doesn't do anything to verify this is correct. That's the problem Fluent NHibernate looks to solve. So I added the references to my Models class, and created a few mapping classes. Here's what the TeamMap class looks like:
1: public class TeamMap : ClassMap<Team>
2: {
3: public TeamMap()
4: {
5: TableName = "cpTeams";
6: Id(x => x.TeamId).GeneratedBy.Native();
7: Map(x => x.Abbreviation);
8: Map(x => x.Location);
9: Map(x => x.NickName);
10: }
11: }
This is much, much better, because now any change in our model (renaming properties, etc.) or a misspelling in our mapping is caught by the compiler. Not only that, but if we're smart about how we make our changes and use a refactoring tool, the mapping gets updated automatically for us. Also, it lets us specify things in one place instead of two. I don't have to spell out the types of each property - it's looking at the entity, so it knows the type already.
But the question still remained for me - can I do the complicated stuff for mapping properties. The answer is yes. Here's the class to map my Game class, which has a few relationships (explained in the previous post):
1: public class GameMap : ClassMap<Game>
2: {
3: public GameMap()
4: {
5: TableName = "cpGames";
6: Id(x => x.GameId).GeneratedBy.Native();
7: References(x => x.Week).WithForeignKey("WeekId").Cascade.All().FetchType.Join();
8: References(x => x.HomeTeam).WithForeignKey("HomeTeamId").Cascade.All().FetchType.Join();
9: References(x => x.AwayTeam).WithForeignKey("AwayTeamId").Cascade.All().FetchType.Join();
10: References(x => x.Season).WithForeignKey("SeasonId").Cascade.All().FetchType.Join();
11: }
12: }
Now, here is where I had an issue. The "WithForeignKey" method doesn't seem to work. It looked for the property name + "_id" to be the field name in the database, and I don't use underscores in my columns. That call should have overridden the conventions used, but it didn't.
After a quick call for help on twitter, I found my answer in the source for Fluent NHibernate. There are conventions that handle how properties and references are mapped to columns when you don't explicitly state them. Turns out it's fairly easy to change those conventions, and you don't even have to change the source of Fluent NHibernate, which is nice. I'll show that, but it's part of the overall configuration for NHibernate. This is in my NHibernateHelper class from the other post:
1: public class NHibernateHelper
2: {
3: private static ISessionFactory _sessionFactory;
4: private static ISessionFactory SessionFactory
5: {
6: get
7: {
8: if (_sessionFactory == null)
9: {
10: var configuration = MsSqlConfiguration
11: .MsSql2005
12: .ConnectionString.Is("server=(local);Initial Catalog=CP;User Id=sa;Password=;")
13: .ShowSql()
14: .ConfigureProperties(new Configuration());
15:
16: var persistenceModel = new PersistenceModel();
17:
18: persistenceModel.Conventions.GetForeignKeyName = (prop => prop.Name + "Id");
19: persistenceModel.Conventions.GetForeignKeyNameOfParent = (prop => prop.Name + "Id");
20: persistenceModel.addMappingsFromAssembly(
21: Assembly.Load(Assembly.GetExecutingAssembly().FullName));
22: persistenceModel.Configure(configuration);
23:
24: _sessionFactory = configuration.BuildSessionFactory();
25: }
26: return _sessionFactory;
27: }
28: }
29:
30: public static ISession OpenSession()
31: {
32: return SessionFactory.OpenSession();
33: }
34: }
This is a big change from my last sample. Part of the reason for that is that I've also eliminated the need to have a hibernate.cfg.xml file - it's all part of the above code. Most of the hibernate.cfg.xml elimination code was borrowed from a post on the NHibernate FAQ blog, but one thing that caught me up a bit was that you don't actually call configuration.Configure() any more, since that looks for the hibernate.cfg.xml file, and will throw an error. Now you call ConfigureProperties() passing in a configuration (line 14). Lines 18 and 19 is where I change the conventions used by Fluent NHibernate from looking for HomeTeam_id to HomeTeamId. The rest is pretty similar to the previous example.
After doing this, I was able to eliminate all of my NHibernate-specific XML files, and everything worked as it did before. All in all, it took about an hour to switch it all over. Not bad, considering what I'm gaining in maintainability down the line.
Categories: Development, Software, C#