Wednesday, January 11, 2012

Mapping-by-Code - Component (and odd cases of Fluent NHibernate's fluency)

Let's move on with mapping-by-code vs. XML vs. Fluent NHibernate. The second post in Ayende's series was about <component> mapping. It's equivalent in mapping-by-code is Component, of course. There are total 6 overloads for Component method - 3 parametrized with lambda expression and 3 with strings for cases where the property is not publicly visible. In each three, there is an overload designed for dynamic components, which are separate topic. So the overloads that interest us have signatures like this:

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

The first parameter is an lambda expression specifying the property of type TComponent. The second is the component mapping itself - it can contain component's properties mapping (and other class mapping parts except Id) and some options defining component's behavior.

Component(x => x.Address, c =>
// mappings for component's parts
c.Property(x => x.City);
c.Property(x => x.ZipCode);
// etc...

// mappings for component's options
c.Parent(x => x.Owner, p => p.Access(Accessor.ReadOnly));
c.Access(Accessor.Property); // or Accessor.Field or Accessor.ReadOnly


Mapping for component's parts can of course contain its own options - i.e. see previous post for Property options.

Parent is a way to create bidirectional relationship between component and its owning entity - it allows mapping back to entity. The only thing that can be customized about this relationship is how should NHibernate access the Parent property in component class - using Access method. This needs to be distinguished from component's Access method, that affects how NHibernate access the opposite - component property in entity. Insert, Update, OptimisticLock and Lazy are about the latter property, too.

One thing is probably missing - both XML and Fluent mapping allows to add Unique constraint on the component, I can't see how to do it with mapping-by-code.

Fluent NHibernate's equivalent

One of the ways to map component in FNH is:

Component(x => x.Address, c =>
// mappings for component's parts
c.Map(x => x.City);
c.Map(x => x.ZipCode);
// etc...

// mappings for component's options
c.ParentReference(x => x.Owner)

c.SqlUpdate("update query")
c.SqlInsert("insert query")
c.SqlDelete("delete query")
c.SqlDeleteAll("delete all query")

ReadOnly is just a FNH's shortcut for setting both .Not.Insert() and .Not.Update().

Note that Fluent NHibernate allows to specify raw SQL queries for updates, inserts and deletes - mapping-by-code doesn't. But this is not mapping-by-code's limitation, it's rather a FNH bug. Mapping raw queries doesn't make a sense for components, that are by definition owned and managed by its owning entity and NHibernate's XML schema doesn't even allow these attributes in component mapping.

Moreover, FNH's fluency introduces a lot of confusion in cases like this:

c.ParentReference(x => x.Owner).Access.ReadOnly();

It may look like defining an access strategy for parent reference, but no - it is defining an access strategy for the component itself and there's no way to set the former in Fluent NHibernate (at least I can't see any).

Note also that there are three alternative ways to attach component's options and all of it seems to work the same:

Component(x => x.Address, c =>
c.LazyLoad().ParentReference(x => x.Owner);
c.ParentReference(x => x.Owner);
.ParentReference(x => x.Owner);

And - continuing the exploration of Fluent's API odd cases - setting access strategy and disabling the component from update and insert queries looks like that:


Well, now I clearly see the limitations of fluent interfaces. In Fluent NHibernate it can sometimes be more confusing than helpful. Mapping-by-code seems to have much more elegant solution here.

1 comment:

  1. Hi,

    Just start trying the Mapping-by-code feature of NHibernate. Everything works as expected except this one:

    I have a component call ContactInfo which is used by many classes. There is a property called "Email" which is not needed in every class using the component. For example, if used as an account Billing Contact, Email will be used. If used in a bill as Payer, Email is not tracked. However, if I map Email in the case of BillingContact, when loading Bill objects, I get

    Invalid column name 'Email'.

    I can see the SQL generated as "SELECT ... bills0_.Email, ...".

    It looks to me that once a component is mapped, the mapping logic is reused and all mapped properties are assumed mapped in any classes using the component. Is this the convention? If so, is there a way to override it?

    Thank you for your help!