Wednesday, January 4, 2012

NHibernate's Mapping by Code - first impressions

Recently I've found some time to check out NHibernate's 3.2 new mapping-by-code feature that allows to completely remove XML mappings from NHibernate's pipeline.

I'm pretty used to using Fluent NHibernate, but I always think of it as the only alternative to plain hbm.xml mappings, that are just too clumsy and too verbose. I like FNH's fluency and clarity, but I think it's far from being ideal solution. My two major objections are that it hides some very important mapping parts like collection types in defaults and it introduces confusion by renaming some NHibernate's concepts without clear reason (i.e. <property> renamed in FNH to Map(), being totally different from original NHibernate's <map>).

I like the idea of integrated code-first mapping solution in NHibernate to replace FNH pretty much for several reasons:

  • there will be no additional library on which our projects depends;
  • there will be no mismatch between assemblies versions and no additional obstacle for upgrading NH; see often discussed workarounds for the lack of Fluent NH package compatible with NH 3.2 on NuGet (there is already the compatible package, but it's not marked as the latest one, surprisingly);
  • the set of features offered by integrated mapping solution will match NHibernate's core features more likely, along with naming conceptions;
  • last but not least, we'll save on XML transformations that is still done by Fluent NHibernate, as native mapping-by-code skips this step completely.

I've took the conformist, class-by-class approach as I feel it's better for complicated models. I was using Fluent NHibernate's automapping, but it often turned out that almost all entities needed some overrides and this reduces the sense for automapping at all. Moreover, I like being explicit about such an important things like database interactions. Each mapping change should trigger careful testing as mappings are quite fragile part of the application. When mapping changes implicitly together with class structure changes because of some kind of automapping or detailed conventions, the impact of such change can be easily overlooked.

The simple mappings are... simple. For anyone using either XML mappings or Fluent NHibernate, there should be no problem to write class maps. One-to-many relationship looks like this:

public class City
{
public virtual int Id { get; protected set; }
public virtual long Population { get; set; }
public virtual string Name { get; set; }
public virtual Municipality Municipality { get; set; }
}

public class CityMap : ClassMapping<City>
{
public CityMap()
{
Id(x => x.Id, m => m.Generator(Generators.Identity));
Property(x => x.Population);
Property(x => x.Name, m => m.Length(SqlClientDriver.MaxSizeForLengthLimitedString + 1));
ManyToOne(x => x.Municipality, m =>
{
m.Cascade(Cascade.All);
m.NotNullable(true);
});
}
}

I've encountered first problems when tried to map many-to-many. It seems like mapping-by-code doesn't come with any useful default naming convention for tables and columns and we need to specify it explicitly in mapping - it looks more like XML in this case. Fluent NH can figure out pretty decent names on its own. I'll probably need to define some conventions to make it resemble Fluent mapping more. Here is what I had to do to make it work (I'm showing one side of the relationship only, but you can see the thing):

public class Street
{
public Street()
{
Districts = new List<District>();
}

public virtual int Id { get; protected set; }
public virtual string Name { get; set; }
public virtual ICollection<District> Districts { get; set; }
}

public class StreetMap : ClassMapping<Street>
{
public StreetMap()
{
Id(x => x.Id, m => m.Generator(Generators.Identity));
Property(x => x.Name, m => m.Length(SqlClientDriver.MaxSizeForLengthLimitedString + 1));

Bag(x => x.Districts, c =>
{
c.Key(k =>
{
k.Column("StreetId");
k.NotNullable(true);
});
c.Cascade(Cascade.All);
c.Table("Streets2Districts");
c.Inverse(true);
}, r => r.ManyToMany(m => m.Column("DistrictId")));
}
}

Now I'm trying to figure out how to map dynamic components, but it's not as easy as it should be. Here is the API I'm trying to use:

public void Component<TComponent>(
Expression<Func<TEntity, IDictionary>> property,
TComponent dynamicComponentTemplate,
Action<IDynamicComponentMapper<TComponent>> mapping)
where TComponent : class

or

protected virtual void RegisterDynamicComponentMapping<TComponent>(
Expression<Func<TEntity, IDictionary>> property,
Action<IDynamicComponentMapper<TComponent>> mapping)
where TComponent : class

Anyway, I don't know what TComponent in case of dynamic components should be. Resources on the web are extremely poor - the only answer I've found was not really helpful.

And here is the main weakness of mapping-by-code for me by now - it's too fresh. There is virtually no documentation apart from a few examples mentioned at StackOverflow. And even the code from initial Fabio Maulo's blog posts introducing the feature are outdated as some things changed before it goes to the production.

It's hard to think about migrating from FNH to mapping-by-code if it is still such an unknown land. Anyway, I think the direction is very good and the beginning is promising. I'll probably try to explore this land on my own.

7 comments:

  1. I must agree 100% with you, the exact same feeling I got left with. The intention is good but it simply feels immature compared to FNH.

    ReplyDelete
  2. Hi Adam,

    Did you figure out how to map the dynamic component?

    I tried using the following approach, but it seems unable to find the correct types.

    We can share some code if we can help each other out.

    I have mapped almost every situation now, besides dynamic components.

    Map(x => x.Attributes,
    mapping =>
    {
    mapping.Key(k => k.Column("CustomerId"));
    mapping.Table("CustomerAttributes");
    },
    mapping => mapping.Element(k => k.Column("propertyName")),
    mapping => mapping.Element(k => k.Column("propertyValue")));

    M. Schopman - famschopman at gmail.com

    ReplyDelete
  3. I've not investigated this issue yet. Will try this week for sure and let you know.

    ReplyDelete
  4. And here it is: http://notherdev.blogspot.com/2012/01/mapping-by-code-dynamic-component.html

    ReplyDelete
  5. How do you get NHibernate to include mapping-by-code in the configuration? Conventionally, you'd declared it in the as , but this doesn't seem to work with mapping-by-code.

    The Exception is: Unable to locate persister for the entity named 'NHibernate.Map.Employee'.

    ReplyDelete
    Replies
    1. Correction:

      Conventionally, you'd declared it in the [hibernate-configuration] as [mapping assembly="CentralDataLayer" /], but this doesn't seem to work with mapping-by-code.

      Delete
    2. Rory - see for example here: http://www.rjp-software.co.uk/2012/05/29/mapping-by-code-building-a-session-factory/

      Delete