One of the most useful things you can do when setting up an NHibernate environment is to sort out of the XSDs so that Intellisense works in Visual Studio. Note that the documentation is rather out of date. Most people are now using the 2.2 schema, not the 2.0 schema, and the Visual Studio directory in 2008 is C:Program FilesMicrosoft Visual Studio 9.0XmlSchemas. Especially notice the “.0”, which is new.
Sorting this out not only allows you to more quickly see errors in your HBM files (a major benefit in itself) but also enables one of my favourite learning approaches: Intellisense exploration. This, in combination with reading the documentation, can give you a fairly broad picture of the functionality. In particular, it provided me with an answer to a question that had been bugging me for a while: how to denormalize data with NHibernate. NHibernate’s support for dealing with database structures that do not map directly to your domain is one of its strongest features, but nearly all examples on the Internet deal with the simple case of mapping directly to a closely related data structure. This is hardly NHibernate’s fault: Microsoft examples suffer from exactly the same problem.
So, here’s the scenario: I’m writing a lot of code that deals with FIX messages. FIX messages do not easily map to database structures, however certain parts of the average message are really useful for searching (generally, the order and execution references that allow you to reconcile the messages against other systems). Now, this produces a use case which favours denormalization:
- You can’t throw information away, so you have to log the full message in its native format.
- Since this isn’t searchable, you need relational fields corresponding to some of the content of the message.
- Furthermore, none of this is in any way related to the domain requirements of the original FIX message class.
So, really what you want to go is to create virtual properties that can be associated with your class, that NHibernate can use and nothing else. This feature is called “Property Accessors”. So, in my HBM file, you write:
<property column="ClOrdId" name="Tag11" type="String" access="ColourCoding.FixPropertyAccessor, ColourCoding" />
Which maps tag 11 of the FIX message to the “ClOrdId” column in the database. FixPropertyAccessor is a class that implements NHibernate.Properties.IPropertyAccessor. Now, when using custom accessors, NHibernate can’t use reflection to determine types. This means that you will need to remember to use the type attribute to specify the target type.
I’ve included an example at the bottom. This one won’t compile until you delete the contents of the “Get” method, since the logic is specific to the application from which this is excerpted.
Aside: There is, in fact, a feature to allow the property accessor itself to give you this information, but it’s pretty much designed only for internal use. If it just obtained the information from the IGetter’s target type, there wouldn’t be a problem. Ho hum.
using System; using System.Text.RegularExpressions; using NHibernate.Properties; namespace ColourCoding { public class FixPropertyAccessor : IPropertyAccessor { public IGetter GetGetter(Type theClass, string propertyName) { return new FixProperty(propertyName); } public ISetter GetSetter(Type theClass, string propertyName) { return new FixProperty(propertyName); } class FixProperty : IGetter, ISetter { int _tagNumber; string _propertyName; TypeCode _typeCode; public FixProperty(string name) { _propertyName = name; _tagNumber = GetTagNumber(name); _typeCode = GetTypeCode(_tagNumber); } static TypeCode GetTypeCode(int tagNumber) { // Stripped down for clarity. switch (tagNumber) { case TagNumbers.SendingTime: return TypeCode.DateTime; case TagNumbers.MsgSeqNum: return TypeCode.Int32; default: return TypeCode.String; } } static Regex __tagRegex = new Regex(@"Tag(?<Id>d+)"); static int GetTagNumber(string propertyName) { Match match = __tagRegex.Match(propertyName); if (match == Match.Empty) { throw new ArgumentException( string.Format("Property name '{0}' should be in the format 'Tag' followed by a number.",
propertyName), "propertyName"); } int result; if (int.TryParse(match.Groups["Id"].Value, out result)) { return result; } throw new ArgumentException( string.Format(
"Property name '{0}' was in the correct format, but the numeric part did not parse as a number.", propertyName), "propertyName"); } public object Get(object target) { // Obviously, this part of the code will not compile on your machine. // I'm including it to give the general flavour of what you'll need to implement. FixMessageTracker tracker = (FixMessageTracker)target; FixMessage message = tracker.OriginalMessage; string value = _tagNumber == TagNumbers.MsgSeqNum
? message.Header[_tagNumber]
: message[_tagNumber]; if (string.IsNullOrEmpty(value)) { return null; } switch (_typeCode) { case TypeCode.Int32: int result; return int.TryParse(value, out result) ? result : 0; case TypeCode.DateTime: return FixMessage.ParseDate(value); default: return value; } } public object GetForInsert(object owner, System.Collections.IDictionary mergeMap,
NHibernate.Engine.ISessionImplementor session) { return Get(owner); } public System.Reflection.MethodInfo Method { // We don't support this feature, so we return null get { return null; } } public string PropertyName { get { return _propertyName; } } public Type ReturnType { get { switch (_typeCode) { case TypeCode.Int32: return typeof(int); case TypeCode.DateTime: return typeof(DateTime); default: return typeof(string); } } } public void Set(object target, object value) { // You can't write to a denormalized FIX property. return; } } } }