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
c.Class<TownHallAddress>();
c.Insert(true);
c.Update(true);
c.OptimisticLock(true);
c.Lazy(true);
});
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)
.Access.ReadOnly()
.Insert()
.Update()
.OptimisticLock()
.LazyLoad()
.ReadOnly()
.Unique();
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:
c.Access.ReadOnly().ReadOnly();
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.
Hi,
ReplyDeleteJust 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!