Ditching ActiveRecord For More NHibernate Love

by Rasmus Kromann-Larsen October 15, 2008 22:19

Introduction

I had a project using an old version of Castle ActiveRecord and NHibernate 1.2. Lately there's been a lot of interesting projects surrounding NHibernate and I've been wanting to make the switch away from the old version of ActiveRecord.

ActiveRecord is a thin layer on top of NHibernate that makes it easier to use and configure, especially through configuration using attributes on classes and properties (hence the ActiveRecord name as seen in Ruby on Rails). However unless you build all the tools yourself, it can be quite the dependency hell to play around with all the new NHibernate toys. So I wanted to eradicate ActiveRecord from my reference list and upgrade NHibernate from 1.2. So I thought I'd share my experiences and some of the useful links I found along the way.

Fluent NHibernate

One of the new things I wanted to try out for NHibernate was Fluent NHibernate, a refreshing new way of doing configuration using a fluent interface in C#. NHibernate is usually configured through XML files that look something like this:

<?xml version="1.0" encoding="utf-16"?>
<hibernate-mapping  auto-import="true" default-lazy="false" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:nhibernate-mapping-2.2">
  <class name="Model.Survey, Model" table="Surveys">
    <id name="Id" access="property" column="Id" type="System.Guid" unsaved-value="00000000-0000-0000-0000-000000000000">
      <generator class="assigned">
      </generator>
    </id>
    <property name="Name" access="property" type="String">
      <column name="Name"/>
    </property>
    <property name="Label" access="property" type="String">
      <column name="Label"/>
    </property>
    <property name="HeaderFile" access="property" type="String">
      <column name="HeaderFile"/>
    </property>
    <property name="FooterFile" access="property" type="String">
      <column name="FooterFile"/>
    </property>
  </class>
</hibernate-mapping>

Actually I don't mind XML files that much, except that they are cumbersome to write. But moving to code has it's merits. First of all you get the static type checking from the compiler, which is nice for avoiding spelling mistakes and such. Secondly, having your mapping directly in code makes it refactor-friendly. Renaming properties will automagically propagate to your mapping and reduce strange errors. However, unless you specify column names for your mapping, you might end up refactoring your database indirectly (depending on how you manage your schema), so keep that in mind. Third of all I like the clarity and readability of the configuration:

    public class SurveyMap : ClassMap<Survey>
    {
        public SurveyMap()
        {
            WithTable("Surveys");

            Id(x => x.Id).GeneratedBy.Assigned();

            Map(x => x.Name);
            Map(x => x.Label);
            Map(x => x.HeaderFile);
            Map(x => x.FooterFile);
        }
    }

Notice the clever use of lambdas and expressions to avoid strings. Nice indeed. The Fluent NHibernate package also includes a some "framework" classes to ease the testing of your database mappings, as outlined in this post by Jeremy Miller.

LINQ to NHibernate

I haven't had much time to try LINQ yet, so getting my NHibernate upgraded meant a chance to try out LINQ to NHibernate. Actually LINQ to NHibernate is scheduled for NHibernate 2.1, which is not out yet, but Daniel Guenter has created a backport for NHibernate 2.0. Playing around with it was a pleasant change from writing SQL or HQL in in strings. I haven't spend that much time on expanding the use of IQueryable<T> into my application yet, but I did convert my repositories so they use LINQ now. I really want to experiment more with this and I think I'll draw some inspiration from this post by Søren Skovsbøll on his repository layout.

Gotchas

I ran into a few issues when doing the actual conversion.

As my focus was to upgrade my version of NHibernate, I didn't want to convert all my mappings to Fluent NHibernate from the beginning, so I wanted to generate the XML mapping files from my ActiveRecord configuration and then use these as my basis for going fluent later. However, I was using an InPlaceConfiguration with ActiveRecord which is really just a glorified dictionary of properties for setting up connection strings and such instead of an XML file or a web.config section. ActiveRecord Configurations have a Debug property which will output the XML mapping files for you, but unfortunately this property was read-only in my old version of ActiveRecord. I actually checked and it seems to be fixed in the ActiveRecord trunk. So I converted my InPlaceConfiguration into an XML file and enabled debug to get my mappings.

There was some breaking changes in NHibernate 2.0, as outlined in this list by Ayende. The only one I ran into was that configuration values no longer were prefixed with "hibernate". This threw me off with some peculiar errors at first until I found the list.

The last thing I ran into was actually a feature from ActiveRecord that I use on application startup called "VerifyModelsAgainstDBSchema". What this does is to verify that the database schema has the necessary table and columns to fit your mapping. Since the application is question is designed to be very drop-able in terms of throwing it into a web directory and starting it up, I've included functionality to create the database if needed. Luckily, this feature is implementing using very little code. If using LINQ to NHibernate, it can be done as easy as:

try
{
    var query = (from o in Session.Linq<Object>()
                 where 1 == 0
                 select o);
    query.ToList();
    IsDBInitialized = true;
}
catch (ADOException ex)
{
    _dbExceptions.Add(ex);
}

The trick (as far as I understand) is that NHibernate supports polymorphism in query engine, so by querying for objects of type Object, you're actually grabbing all the tables in your mapping and thus checking the consistency - since the SQL statement will fail on invalid table and/or column names. By adding the always false statement 1 == 0, you make sure that you don't actually pull anything from the database. Clever.

Conclusion

In this post I described my 8-hour hike from an ancient version of ActiveRecord to NHibernate 2.0. It was actually very smooth and the problems were easily solved. I'm really looking forward to working more with these new exciting technologies. Enjoy.

kick it on DotNetKicks.com

Tags: , ,

NHibernate

Comments

10/15/2008 10:27:10 PM #

trackback

Trackback from DotNetKicks.com

Ditching ActiveRecord For More NHibernate Love

DotNetKicks.com

10/16/2008 12:04:03 AM #

michael

Make sure you check out the attribute-based entities! For simple databases and tables, it is easier than having a separate XML file.   For example:


using NHibernate;
using NHibernate.Tool.hbm2ddl;
using NHibernate.Mapping.Attributes;
using Configuration=NHibernate.Cfg.Configuration;

[Serializable]
[Class(Name = "MyLib.OrderDetail, MyLib, Culture=neutral", Table = "tbl_OrderDetail", Lazy = false)]
public class OrderDetail : IOrderDetail
{
    [Id(0, Name = "Id", TypeType = typeof(Int32), UnsavedValue = "0")]
    [Column(1, Name = "Id", SqlType = "int", Index = "PK_OrderDetail_Id", Unique = true, NotNull = true)]
    [Generator(3, Class = "identity")]
    public virtual int Id { get; private set; }

    [Property(Length = 50)]
    public virtual string OrderNumber { get; set; }
}

public sealed class DAL
{

    //private const string TablePrefix = "tbl_";
    private static readonly string[] AppTables = { "[dbo].[tbl_OrderDetail]" };
    private static readonly string[] SqlUsers = {"webuser"};
    private static ISessionFactory sessionFactory;
    private static Configuration config;

    public static DateTime DateTime_MinValue = DateTime.Parse("1/1/1753 12:00:00 AM"); // necessary for non nullable SqlDateTime columns

    public static string SchemaFile
    {
        get
        {
            if (ContextStorage.IsWebContext())
                return HttpContext.Current.Server.MapPath(ConfigurationManager.AppSettings["SchemaFilename"]);
            return ConfigurationManager.AppSettings["SchemaDirectory"] + ConfigurationManager.AppSettings["SchemaFilename"];
        }
    }

    public static string AlternativeSchemaFile
    {
        get
        {
            return SchemaFile.Replace(ConfigurationManager.AppSettings["SchemaFilename"], "temp\\" + ConfigurationManager.AppSettings["SchemaFilename"]);
        }
    }

    private static void InitConfiguration()
    {
        if (config != null) return;
        config = new Configuration();
        config.SetProperty("hibernate.connection.connection_string", ConnectionProvider.DbConnectionString);
        config.SetProperty("hibernate.connection.driver_class", "NHibernate.Driver.SqlClientDriver");
        //config.SetProperty("hibernate.connection.isolation", "ReadCommitted");
        //config.SetProperty("hibernate.connection.pool_size", "10");
        //config.SetProperty("hibernate.connection.provider", "NHibernate.Connection.DriverConnectionProvider");
        config.SetProperty("hibernate.connection.provider", "MyLib.ConnectionProvider, MyLib");
        config.SetProperty("hibernate.dialect", "NHibernate.Dialect.MsSql2000Dialect");
        config.AddAssembly("MyLib");

        //config.SetProperty("hibernate.adonet.wrap_result_sets", "false");
        //config.SetProperty("hibernate.adonet.batch_size", "10");
        //config.SetProperty("hibernate.bytecode.provider", "lcg");
        //config.SetProperty("hibernate.cache.provider_class", "NHibernate.Cache.HashtableCacheProvider, NHibernate");
        //config.SetProperty("hibernate.cache.use_minimal_puts", "false"); // useful for clustered caches
        //config.SetProperty("hibernate.cache.use_query_cache", "true");
        //config.SetProperty("hibernate.cache.query_cache_factory", "StandardQueryCacheFactory");
        //config.SetProperty("hibernate.cache.region_prefix", "cash_");
        //config.SetProperty("hibernate.default_schema", "schema_name");
        //config.SetProperty("hibernate.hbm2ddl.auto", "create-drop");
        //config.SetProperty("hibernate.max_fetch_depth", "3");
        //config.SetProperty("hibernate.prepare_sql", "false");
        //config.SetProperty("hibernate.query.startup_check", "false");
        config.SetProperty("hibernate.query.substitutions", "true 1, false 0, yes 'Y', no 'N'");
        config.SetProperty("hibernate.show_sql", "false");
        //config.SetProperty("hibernate.transaction.factory_class", "AdoNetTransactionFactory");
        //config.SetProperty("hibernate.use_proxy_validator", "true");
        //config.SetProperty("hibernate.use_reflection_optimizer", "true");

        // see www.hibernate.org/.../mapping-attributes.html
        NHibernate.Mapping.Attributes.HbmSerializer.Default.Validate = true;
        config.AddInputStream(
            NHibernate.Mapping.Attributes.HbmSerializer.Default.Serialize(
                System.Reflection.Assembly.GetExecutingAssembly()));
    }

    private static string WorkingSchemaFile()
    {
        return (File.Exists(SchemaFile) ? SchemaFile : AlternativeSchemaFile);
    }

    private static void InitSessionFactory()
    {
        sessionFactory = config.BuildSessionFactory();
    }

    public static void AddCfg(Type type)
    {
        InitConfiguration();
        config.AddClass(type);
    }

    public static int ExecuteNonQuery(string Sql)
    {
        using (var cn = ConnectionProvider.GetOpenedSqlConnection())
        {
            var cmd = cn.CreateCommand();
            cmd.CommandText = Sql;
            return cmd.ExecuteNonQuery();
        }
    }

    public static void ResetDB()
    {
        InitConfiguration();
        ExportDBSchema();
        AppendDBPermissions();
        PatchExportedSchema();
    }

    private static void PatchExportedSchema()
    {
        var stream = ReadDBSchema();
        stream = stream.Replace("CreatedOn DATETIME not null,", "CreatedOn DATETIME not null default (getdate()),");
        using (var sw = new StreamWriter(WorkingSchemaFile(), false))
            sw.Write(stream);
    }

    private static void ExportDBSchema()
    {
        var schExp = new SchemaExport(config);
        schExp.SetOutputFile(SchemaFile);
        try
        {
            schExp.Create(false, false);
        }
        catch (Exception)
        {
        }
        if (WorkingSchemaFile() == SchemaFile) return;
        schExp = new SchemaExport(config);
        schExp.SetOutputFile(AlternativeSchemaFile);
        schExp.Create(false, false);
    }

    /// <summary>
    /// Note that user needs special permissions to import schema.
    /// </summary>
    private static void ImportDBSchema()
    {
        ExecuteNonQuery(ReadDBSchema());
    }

    private static string ReadDBSchema()
    {
        using (var sr = new StreamReader(WorkingSchemaFile()))
            return sr.ReadToEnd();
    }

    private static void AppendDBPermissions()
    {
        var Permissions =
            new[]
                {
                    "GRANT ALTER",
                    "GRANT DELETE",
                    "GRANT INSERT",
                    "GRANT SELECT",
                    "GRANT UPDATE"
                };
        using (var sw = new StreamWriter(WorkingSchemaFile(), true))
        {
            sw.WriteLine(";");
            foreach (var item in Permissions)
            {
                foreach (var table in AppTables)
                {
                    foreach (var user in SqlUsers)
                    {
                        sw.WriteLine(item + " ON " + table + " TO " + user + ";");
                    }
                }
            }
        }
    }

    private static readonly Regex SchemaRegEx = new Regex(@"\[(.*?)\]\.\[(.*?)\]", RegexOptions.Compiled);
    private static string GetSchemaName(string table)
    {
        var match = SchemaRegEx.Match(table);
        if (!match.Success) throw new InvalidDataException("table parameter is not formatted correctly.");
        return SchemaRegEx.Match(table).Groups[1].Value;
    }

    private static string GetTableName(string table)
    {
        var match = SchemaRegEx.Match(table);
        if (!match.Success) throw new InvalidDataException("table parameter is not formatted correctly.");
        return SchemaRegEx.Match(table).Groups[2].Value;
    }
    
    public static bool TablesExist()
    {
        var RefCount = 0;
        using (var cn = new SqlConnection(ConnectionProvider.DbConnectionString))
        {
            cn.Open();
            foreach (var table in AppTables)
            {
                using (var cm =
                    new SqlCommand(
                        "SELECT count(*) FROM INFORMATION_SCHEMA.tables WHERE TABLE_TYPE='BASE TABLE' AND TABLE_NAME='" + GetTableName(table) + "'", cn))
                    RefCount += (int) cm.ExecuteScalar();
            }
        }
        return (RefCount == AppTables.Length);
    }

    public static ISession CurrentSession(bool UseTransaction)
    {
        InitConfiguration();
        if (sessionFactory == null)
            InitSessionFactory();
        var currentSession = ContextStorage.Items[ConfigurationManager.AppSettings["CurrentSessionKey"]] as ISession;
        if (currentSession == null)
        {
            currentSession = sessionFactory.OpenSession();
            ContextStorage.Items[ConfigurationManager.AppSettings["CurrentSessionKey"]] = currentSession;
            if (UseTransaction)
                ContextStorage.Items[ConfigurationManager.AppSettings["CurrentTransactionKey"]] = currentSession.BeginTransaction();
        }
        return currentSession;            
    }

    public static ISession CurrentSession()
    {
        return CurrentSession(false);
    }

    public static void CloseSession()
    {
        var currentSession = ContextStorage.Items[ConfigurationManager.AppSettings["CurrentSessionKey"]] as ISession;
        if (currentSession == null) return;
        var currentTransaction = ContextStorage.Items[ConfigurationManager.AppSettings["CurrentTransactionKey"]] as ITransaction;
        if (currentSession.IsConnected)
        {
            if (currentTransaction != null)
                currentTransaction.Commit();
            currentSession.Close();
        }
        if (ContextStorage.Items.Contains(ConnectionProvider.DbConnection))
        {
            ((IDbConnection)ContextStorage.Items[ConnectionProvider.DbConnection]).Close();
            ContextStorage.Items.Remove(ConnectionProvider.DbConnection);
        }                
        ContextStorage.Items.Remove(ConfigurationManager.AppSettings["CurrentSessionKey"]);
        if (currentTransaction != null)
            ContextStorage.Items.Remove(ConfigurationManager.AppSettings["CurrentTransactionKey"]);
    }

    public static void CloseSessionFactory()
    {
        if (sessionFactory != null)
            sessionFactory.Close();
    }
}

public class ConnectionProvider : DriverConnectionProvider
{
    // blechie.com/.../...-Mans-Shards-in-NHibernate.aspx
    public const string DbConnection = "DbConnection";
    public static string DbConnectionString = ConfigurationManager.ConnectionStrings["DBConnectString"].ConnectionString;
    public IDbConnection GetConnection(bool IsNHibernate)
    {
        IDbConnection conn = null;
        if (ContextStorage.Items.Contains(DbConnection))
            conn = (SqlConnection)ContextStorage.Items[DbConnection];
        if (conn == null)
        {
            if (IsNHibernate)
            {
                if (string.IsNullOrEmpty(DbConnectionString))
                    return base.GetConnection();
                conn = Driver.CreateConnection();
            }
            else
                conn = new SqlConnection(DbConnectionString);
            ContextStorage.Items[DbConnection] = conn;
        }
        if (string.IsNullOrEmpty(conn.ConnectionString))
            conn.ConnectionString = DbConnectionString;
        if (conn.State == ConnectionState.Closed)
            conn.Open();
        return conn;
    }

    public override IDbConnection GetConnection()
    {
        return GetConnection(true);
    }

    public static IDbConnection GetOpenedSqlConnection()
    {
        return new ConnectionProvider().GetConnection(false);
    }
}

public class ContextStorage
{

    [ThreadStatic]
    private static Dictionary<string, object> threadContext = new Dictionary<string, object>();

    public static bool IsWebContext()
    {
        try
        {
            return (HttpContext.Current != null);
        }
        catch (HttpException) { }
        return false;
    }

    public static IDictionary Items
    {
        get
        {
            return IsWebContext() ? HttpContext.Current.Items : threadContext;
        }
    }

}

michael United States

11/24/2008 2:53:49 PM #

Eric Nicholson

Very interesting! We're considering doing the same thing, so this will be a huge help.

Eric Nicholson United States

Comments are closed

Powered by BlogEngine.NET 1.6.1.0
Theme by Mads Kristensen | Modified by Mooglegiant | Adjusted by Rasmus Kromann-Larsen

About Me

I am a danish .NET developer blogging about the technical side of my life, mostly .NET stuff, but also fundamental topics like design patterns, principles and productivity boosters.

In addition, I am a core group member of Aarhus .NET User Group.