Wednesday 20 July 2011

Mapping conventions using mapping by code

I asked the following question on the NHUsers google group; "I am playing with mapping by code and started wondering about conventions."

According to WIKI:-
Conventions may be formalized in a documented set of rules that an entire team or company follows, or may be as informal as the habitual coding practices of an individual.
All of my mapping files share the same common functionality:-

1. All Bags have bag.Cascade(Cascade.All | Cascade.DeleteOrphans) and bag.BatchSize(10)
2. All table names are lowercase Table("organisation");
3. All bags have the action => action.OneToMany()

Normally in NHibernate 3.2 I would define all my mapping classes using this type of syntax:-
internal class OrganisationMapping : ClassMapping<Organisation> {
  public OrganisationMapping() {
    Id(x => x.Id, map => map.Generator(Generators.GuidComb));
    Property(x => x.Name, x => x.NotNullable(true));
    Table("organisation");

    Bag(x => x.IncomeTypeList, bag => {
      bag.Key(k => k.Column(col => col.Name("OrganisationId")));
      bag.Cascade(Cascade.All | Cascade.DeleteOrphans);
      bag.BatchSize(20);
    }, action => action.OneToMany());
  }
}
The problem I have is that I have 20 or so of these class mappings to do and most of my bags share similar properties. This is what led me to think about conventions and a way to make my code a) compact, b) follow similar standards and c) allow me to map my classes quicker without having to think about what I had set before.

So how do we solve this problem? First I read this blog post from Fabio Maulo then I decided to have a go.

Below is a snippet of my configuration code:-
var mapper = new ModelMapper();
mapper.AddMappings(typeof(Organisation).Assembly.GetTypes());

var configure = new Configuration();
configure.DataBaseIntegration(x => {
  x.Dialect<MySQL5Dialect>();
  x.ConnectionStringName = "db";
}).CurrentSessionContext<WebSessionContext>();

var mapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
configure.AddDeserializedMapping(mapping, "Domain");

SessionFactory = configure.BuildSessionFactory();
The first problem is how do I make all my bags have a batch size of 20 and set Cascade.All | Cascade.DeleteOrphans

This is really easy I just need to set a convention before my bag gets mapped:-
mapper.BeforeMapBag += (mi, t, map) => {
  map.BatchSize(20);
  map.Cascade(Cascade.All | Cascade.DeleteOrphans);
};
Note: this code gets inserted just after var mapper = new ModelMapper();

The second problem is how do I make all my table names lowercase. Again this convention is extremely easy to achieve:-
mapper.BeforeMapClass +=
  (mi, t, map) => map.Table(t.Name.ToLowerInvariant());
mapper.BeforeMapJoinedSubclass += 
  (mi, t, map) => map.Table(t.Name.ToLowerInvariant());
mapper.BeforeMapUnionSubclass += 
  (mi, t, map) => map.Table(t.Name.ToLowerInvariant());
Note: If you are using subclassess in your NHibernate project then you will also need to define the convention here as well.

The third problem is how do I remove the need to have action => action.OneToMany() defined on all my mappings. If you do not specify this action then you mapping files will contain:-
<bag name="IncomeTypeList" cascade="all,delete-orphan" batch-size="20">
  <key column="Id" />
  <element type="Domain.Model.Entities.IncomeType, Domain.Model" />
 </bag>
but what we really want is:-
<bag name="IncomeTypeList" cascade="all,delete-orphan" batch-size="20">
  <key column="Id" />
  <one-to-many class="IncomeType" />
 </bag>
This one is a little trickier to achieve. According to Fabio:-
We can’t define the one-to-many relation using the ModelMapper because the responsibility to define/discover all relation is delegated to the IModelInspector injected to the ModelMapper instance
So lets look at the solution, first we need to define a new ModelInspector that inherits from ExplicitlyDeclaredModel then override the IsOneToMany method:-
public class MyModelInspector: ExplicitlyDeclaredModel
{
    public override bool IsOneToMany(MemberInfo member)
    {
        if(IsBag(member))
            return true;
        return base.IsOneToMany(member);
    }
}
To use our ModelInspector we need to slightly change the configuration code so we pass the ModelInspector into the ModelMapper:-
var modelInspector = new MyModelInspector();
var mapper = new ModelMapper(modelInspector);
and viola all works as expected!

It should also be pointed out that all these conventions can be overridden by explicitly specifying your requirements on the individual classes themselves.

I have to say at first when I looked at this mapping by code syntax it looked scary and complex. However after a couple of hours playing with it and actually coding my application I have found it quite pleasing and fairly simple (with help from Fabio). I am now looking forward to mapping my domains using this syntax rather than the tried and trusted xml that I have used in the past.

If you haven't already looked at the mapping by code (new in NHiberbate 3.2) then I suggest you do as it really is cool and according to Fabio sexy, but that is his personal taste!

No comments:

Post a Comment