Mapping Timestamp Data Using NHibernate’s ICompositeUserType
In my previous post, I took some string data and mapped it directly to a boolean property on an entity. That was pretty simple, but I wanted to try it out on a little more complex object..
In our projects, most of the entities have a Timestamp property which is of type Timestamp:
public class User { public string UserID { get; set; } public string Username { get; set; } public string Password { get; set; } public Timestamp Timestamp { get; set; } }
and here’s the Timestamp class:
public class Timestamp { public string CreatedByStaff { get; set; } public string UpdatedByStaff { get; set; } public DateTime? CreatedDateTime { get; set; } public DateTime? UpdatedDateTime { get; set; } }
In the database, the USER table would have the obvious columns for UserID, Username, and Password, but also have the columns for the Timestamp which in my case are CREATED_BY_STAFF, UPDATED_BY_STAFF, CREATED_DATETIME, and UPDATED_DATETIME. Now this can easily be mapped as it is by using a component like so:
<component name="Timestamp" class="XYZ.Core.Timestamp, XYZ.Core"> <property name="CreatedByStaff" type="String" column="CREATED_BY_STAFF" not-null="true"/> <property name="UpdatedByStaff" type="String" column="UPDATED_BY_STAFF" not-null="false"/> <property name="CreatedDateTime" type="DateTime" column="CREATED_DATETIME" not-null="true"/> <property name="UpdatedDateTime" type="DateTime" column="UPDATED_DATETIME" not-null="false"/> </component>
That works well, but I hate having to repeat that all over the place. I would rather just have one single line that maps a Timestamp. I experimented with creating a custom mapping type using the ICompositeUserType and got a little closer to the goal. Here’s the class:
public class TimestampMappingType : ICompositeUserType { public bool IsMutable { get { return true; } } public Type ReturnedClass { get { return typeof(Timestamp); } } public string[] PropertyNames { get { return new[] { "CreatedByStaff", "UpdatedByStaff", "CreatedDateTime", "UpdatedDateTime" }; } } public IType[] PropertyTypes { get { return new[] { NHibernateUtil.String, NHibernateUtil.String, NHibernateUtil.DateTime, NHibernateUtil.DateTime}; } } public object Assemble(object cached, ISessionImplementor session, object owner) { return DeepCopy(cached); } public object GetPropertyValue(object component, int property) { var timestamp = AsTimestamp(component); switch(property) { case 0: return timestamp.CreatedByStaff; case 1: return timestamp.UpdatedByStaff; case 2: return timestamp.CreatedDateTime; case 3: return timestamp.UpdatedDateTime; default: throw new YourException("No implementation for property index of '{0}'.", property); } } public void SetPropertyValue(object component, int property, object value) { if (component == null) throw new ArgumentNullException("component"); var timestamp = AsTimestamp(component); switch (property) { case 0: timestamp.CreatedByStaff = (string)value; break; case 1: timestamp.UpdatedByStaff = (string)value; break; case 2: timestamp.CreatedDateTime = (DateTime?)value; break; case 3: timestamp.UpdatedDateTime = (DateTime?)value; break; default: throw new YourException("No implementation for property index of '{0}'.", property); } } public object NullSafeGet(IDataReader dr, string[] names, ISessionImplementor session, object owner) { var createdByStaff = NHibernateUtil.String.NullSafeGet(dr, names[0]); var updatedByStaff = NHibernateUtil.String.NullSafeGet(dr, names[1]); var createdDateTime = NHibernateUtil.DateTime.NullSafeGet(dr, names[2]); var updatedDateTime = NHibernateUtil.DateTime.NullSafeGet(dr, names[3]); return new Timestamp { CreatedByStaff = (string)createdByStaff, UpdatedByStaff = (string)updatedByStaff, CreatedDateTime = (DateTime?)createdDateTime, UpdatedDateTime = (DateTime?)updatedDateTime }; } public void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session) { if (value == null) { ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value; ((IDataParameter)cmd.Parameters[index+1]).Value = DBNull.Value; ((IDataParameter)cmd.Parameters[index+2]).Value = DBNull.Value; ((IDataParameter)cmd.Parameters[index+3]).Value = DBNull.Value; } else { var timestamp = AsTimestamp(value); ((IDataParameter)cmd.Parameters[index]).Value = (object)timestamp.CreatedByStaff ?? DBNull.Value; ((IDataParameter)cmd.Parameters[index + 1]).Value = (object)timestamp.UpdatedByStaff ?? DBNull.Value; ((IDataParameter)cmd.Parameters[index + 2]).Value = (object)timestamp.CreatedDateTime ?? DBNull.Value; ((IDataParameter)cmd.Parameters[index + 3]).Value = (object)timestamp.UpdatedDateTime ?? DBNull.Value; } } public object DeepCopy(object value) { if(value == null) return null; var original = AsTimestamp(value); return new Timestamp { CreatedByStaff = original.CreatedByStaff, UpdatedByStaff = original.UpdatedByStaff, CreatedDateTime = original.CreatedDateTime, UpdatedDateTime = original.UpdatedDateTime }; } public object Disassemble(object value, ISessionImplementor session) { return DeepCopy(value); } public object Replace(object original, object target, ISessionImplementor session, object owner) { return DeepCopy(original); } public new bool Equals(object x, object y) { if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; return x.Equals(y); } public int GetHashCode(object x) { return x == null ? typeof(Timestamp).GetHashCode() + 321 : x.GetHashCode(); } private static Timestamp AsTimestamp(object value) { if (value == null) return null; var ts = value as Timestamp; if(ts == null) throw new YourException("Expected '{0}' but recieved '{1}'.", typeof(Timestamp), value.GetType()); return ts; } }
Using this class in my mapping, I can now shorten the Timestamp mapping to:
<property name="Timestamp" type="XYZ.DataAccess.TimestampMappingType, XYZ.DataAccess"> <column name="CREATED_BY_STAFF" /> <column name="UPDATED_BY_STAFF" /> <column name="CREATED_DATETIME" /> <column name="UPDATED_DATETIME" /> </property>
That’s a little better but I was hoping I could get away with not having to define the columns (they’re the same on every table) but I don’t see a way to set defaults (maybe I’m in the wrong place?). In the end, I’m not sure this really buys me much more than just mapping Timestamp as a component, but I’ll poke around a little more to see if I can figure it out.