One of the downsides of being confronted with a shared legacy database day in and day out is that you have to map your domain objects to database tables that are also used by other applications. A typical scenario in this case is that those database tables contain more columns than those that are required for your application. These extra columns are specifically there to serve those other legacy applications. Heck, to make matters even worse, there are probably some new columns added specifically for your application as well. This is the fairy tale of shared legacy databases.
Using NHibernate in these scenarios can be challenging sometimes but its built in flexibility and extensibility really helps you to deal with those cases. The problem I ran into last week was that we needed to store a domain object into a table that had a lot more columns than were actually required for our application. If it would be possible to store null values in these columns or if they had default values configured for them in the schema, then this would not be a problem. Instead, these unnecessary columns could not store null values and had no default values associated with them.
First option would be to make some changes to the schema of the table. Alas, no luck there because the other legacy applications that are using the same table would break. Now what?
We needed to insert the default values ourselves, but those columns are not known by NHibernate because they are not mapped to any members of the domain object. One way to solve this, is to pollute the domain object by adding private fields that are initialized to the required default values.
public class SomeDomainEntity { // Legacy fields with no purpose for the domain but required // by the database. private Int32 _legacyField1 = 2; private Boolean _legacyField2 = false; private String _legacyField3 = ""; }
This is probably the simplest option, but imposes a broken window as infrastructure concerns are bleeding into the domain this way. In other words, this is not a viable solution. Keeping the legacy stuff isolated as much as possible, NHibernate provides some ways to deal with this by providing an extensive extensibility model.
After some snooping around in the source code of NHibernate, the solution we chose for dealing with this issue is by creating a custom access strategy. The built in property access strategies are probably already well known, but its also possible to write your own access strategy by implementing the IPropertyAccessor interface.
public class SomeDomainObjectAccessor : IPropertyAccessor { private IEnumerable<IGetter> _defaultValueGetters; public SomeDomainObjectAccessor() { _defaultValueGetters = new List<IGetter>() { { new DefaultValueGetter<Int32>("LegacyColumn1", 2) } { new DefaultValueGetter<Boolean>("LegacyColumn2", false) } { new DefaultValueGetter<String>("LegacyColumn3", 2) } } } public IGetter GetGetter(Type type, String propertyName) { return _defaultValueGetters .Where(getter => getter.PropertyName == propertyName) .SingleOrDefault(); } public ISetter GetSetter(Type type, String propertyName) { return new NoopSetter(); } public Boolean CanAccessTroughReflectionOptimizer { get { return true; } } } private class DefaultValueGetter<T> : IGetter { private readonly String _propertyName; private T Value { get; set; } public DefaultValueGetter(String propertyName, T value) { _propertyName = propertyName; Value = value; } public Object Get(Object target) { return Value; } public Type ReturnType { get { return typeof(T); } } public String PropertyName { get { return _propertyName; } } public MethodInfo Method { get { var method = typeof(BasicPropertyAccessor) .GetMethod("GetGetterOrNull", BindingFlags.Static | BindingFlags.NonPublic); var result = (BasicPropertyAccessor.BasicGetter)method .Invoke(null, new Object[] { GetType(), "Value" }); return result.Method; } } public object GetForInsert(Object owner, IDictionary mergeMap, ISessionImplementor session) { return Get(owner); } } private sealed class NoopSetter : ISetter { public void Set(Object target, Object value) {} public String PropertyName { get { return null; } } public MethodInfo Method { get { return null; } } }
This simply involves a getter for providing default values and a dummy setter as we’re not interested in setting any values on the domain objects. The DefaultValueGetter class uses a trick so that we can keep using the reflection optimizer of NHibernate. This also seems to be necessary when using NHibernate Profiler.
Now we only have to provide some properties in the mapping of the domain object using our custom access strategy:
<property name="LegacyColumn1" column="LegacyColumn1" not-null="true" type="Int32" access="SomeNamespace.SomeDomainObjectAccessor, SomeAssembly"/> <property name="LegacyColumn2" column="LegacyColumn2" not-null="true" type="Boolean" access="SomeNamespace.SomeDomainObjectAccessor, SomeAssembly"/> <property name="LegacyColumn3" column="LegacyColumn3" not-null="true" type="String" access="SomeNamespace.SomeDomainObjectAccessor, SomeAssembly"/>
This is probably not the best solution, but it does the job and prevents polluting the domain objects as a result of database quirks like these. I’m interested in hearing feedback or any better approaches.
Anyway, the easy extensibility of NHibernate makes it the best data access solution around. This way, one can deal with all edge case scenarios that weren’t anticipated by the framework builders.
Till next time
Why isn’t the ‘broken window’ option better? Aren’t you writing a lot more code for no other reason than some abstract principle?
Keeping the domain classes as lean as possible and trying to avoid as many infrastructure concerns as I can is not something I consider an abstract principle. Sure it involves some extra efforts, but the gain here is maintainability. If a couple of extra lines of code prevents poor DB management to cripple the application down to its core, then I guess they are worth it.
Entirely agreed Jan, pollution of your domain layer is a catastrophe waiting to happen.
I once worked at a place where objects actually maintained their own population and persistence methods internally. It was ridiculous to maintain once you needed to work with an object in even a shred of a way different than it was originally created for.
I have run into some of these exact problems in multiple shops. Your custom access strategy is interesting, but the database and other systems are still polluting your code.
The approach I eventually settled was to isolate the database with using the Interface Segregation Principle implemented with Views. Create a SQL View that bridges the gap between what your Domain cares about and the realities of the database. For Inserts/Updates, you can either use Updatable Views or Stored Procedures to handle the default data. Updatable Views are a little cleaner because NHibernate sees it as a table and no extra mapping is required. I realize that you’re shifting the complexity out of the code and NHibernate into having to write some SQL. But it seems a cleaner solution to me. An additional benefit is hopefully someday the legacy application will go away and the views will eventually be replaced by actual schema and you can keep your mappings and just change the table name.
In the event that these legacy issues present themselves on all your tables I might be more inclinded to follow your approach. Writing a significant amount SQL means you lose the advantages of NHibernate.
Nice touch, the idea of using that PropertyAccessor to protect your domain model from the database scheme. Surely shows one of the many extensibility mechanism that NH offers.
However, I must say that I like the idea of the views as proposed by Alan as well: overall performance will be better in the end, and the creation of a view on a single database table is a relative simple operation. In my experience, legacy databases also often tend to have cryptic column names etc., and you could take the occasion to name fields on the views such that mapping in e.g. FluentHibernate becomes more convention based …
@Alan @Serge In my case, I can’t have views (because DBA won’t allow it) and I can’t have stored procedures (well, because simply not supported by the DB at hand). I agree that its still pollution of code, but its infrastructure code. This kind of code is the only place that should get infected by poor DB management and decision making instead of the entire code base. If I had other options (like a view or SP) then keeping this kind of mess out of the application might be a viable option as well, I agree.
Nice post
http://d4dilip.wordpress.com/2011/09/27/user-nhibernate-with-legacy-database/