Question

I'm working on an application that will store "commands" in a database using NHibernate. Each command will have a set of arguments with it. For instance, the "sleep" command has a duration, and "set text" has a string value. All of these commands derive from the same Command base type.

I'd like to allow additional commands to be added in the future with the smallest possible impact to the database. My initial reaction is to use the table-per-hierarchy pattern since the only schema modification required would be to add a column to the Command table.

I also considered using the TPH pattern but mapping generic columns instead of specific ones, and then convert them to the specific (strongly-typed) property values in the classes themselves (i.e. shadow the mapped, generic string properties with strongly-typed public properties). That way I wouldn't have to change the table at all if I had a number of columns equal to the most arguments any command could need. These seemed a little hacky in my mind though...

As a relative new-comer to database design and NHibernate usage, can someone point holes in these approaches, or suggest something better? I'm trying to avoid changing the schema (as much as possible) while allowing for future extension and a simple C# API.

Was it helpful?

Solution

Look into a BaseImmutableUserType<T> : IUserType implementation, this would allow you to use the generic column.

OTHER TIPS

IMO, your best bet is to use a table containing a single XML type column instead of baking in an inheritance hierarchy that may or may not model the actual domain problem. XML columns are well-known to be a frictionless way to evolve a data model, especially one which you anticipate may change significantly over time.

The XML column can store the entire object graph needed to represent your command object(s) as serialized by the .NET BCL classes or using your own custom XML serializer (see IXmlSerializable).

NHibernate natively supports the XML SQL Server column type. Googling should bring back several examples of how to do the mapping etc.

I ended up taking the answer from @mxmissile, and here are the details of the interesting parts of the implementation, hopefully helping someone else. Ended up being pretty clean overall, all of the logic is handled in the mappings.

/// <summary>NHibernate class mapping file for <see cref="Action"/>.</summary>
internal sealed class ActionMapper : ClassMap<Action>
{
    /// <summary>Constructor.</summary>
    public ActionMapper()
    {
        DiscriminateSubClassesOnColumn("ClassType").Not.Nullable();

        Id(x => x.Id);
    }
}

/// <summary>NHibernate class mapping file for <see cref="SetText"/>.</summary>
internal sealed class SetTextMapper : SubclassMap<SetText>
{
    public SetTextMapper()
    {
        DiscriminatorValue(typeof(SetText).Name);

        Map(x => x.Text).Column("Arg1").CustomType<StringArgType>();
    }
}

/// <summary>NHibernate class mapping file for <see cref="Sleep"/>.</summary>
internal sealed class SleepMapper : SubclassMap<Sleep>
{
    public SleepMapper()
    {
        DiscriminatorValue(typeof(Sleep).Name);

        Map(x => x.Duration).Column("Arg1").CustomType<TimeSpanArgType>();
    }
}

internal class StringArgType : BaseImmutableUserType<String>
{
    public override SqlType[] SqlTypes
    {
        // All arguments map to strings in the database
        get { return new[] {new SqlType(DbType.String)}; }
    }

    public override object NullSafeGet(IDataReader Reader, string[] Names, object Owner)
    {
        return NHibernateUtil.String.NullSafeGet(Reader, Names[0]).As<String>();
    }

    public override void NullSafeSet(IDbCommand Command, object Value, int Index)
    {
        NHibernateUtil.String.NullSafeSet(Command, Value, Index);
    }
}

internal class TimeSpanArgType : BaseImmutableUserType<TimeSpan>
{
    public override SqlType[] SqlTypes
    {
        // All arguments map to strings in the database
        get { return new[] {new SqlType(DbType.String)}; }
    }

    public override object NullSafeGet(IDataReader Reader, string[] Names, object Owner)
    {
        return NHibernateUtil.TimeSpan.NullSafeGet(Reader, Names[0]).As<TimeSpan?>();
    }

    public override void NullSafeSet(IDbCommand Command, object Value, int Index)
    {
        object val = DBNull.Value;

        if (Value != null)
        {
            TimeSpan timespan = (TimeSpan)Value;
            val = timespan.Ticks;
        }

        NHibernateUtil.String.NullSafeSet(Command, val, Index);
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top