Monday, January 16, 2012

Mapping-by-Code - inheritance

Next article in Ayende's NHibernate mapping series is about inheritance mappings. As we know, there are few different approaches to inheritance in database. Let's see how we can map it using NHibernate's 3.2 mapping-by-code.

Let's use the same model as Ayende did:

Single table

And let's begin with single table approach (table per hierarchy):

public class PartyMap : ClassMapping<Party>
{
public PartyMap()
{
Id(x => x.Id, i => i.Generator(Generators.HighLow));

Discriminator(x =>
{
x.Force(true);
x.Formula("arbitrary SQL expression");
x.Insert(true);
x.Length(12);
x.NotNullable(true);
x.Type(NHibernateUtil.String);

x.Column("discriminator");
// or...
x.Column(c =>
{
c.Name("discriminator");
// c.Length(21);
// c.NotNullable(false);
// etc.
});
});
}

public class CompanyMap : SubclassMapping<Company>
{
public CompanyMap()
{
DiscriminatorValue("Company");
Property(x => x.CompanyName);
}
}

public class PersonMap : SubclassMapping<Person>
{
public PersonMap()
{
DiscriminatorValue("Person");
Property(x => x.FirstName);
}
}
}

Single table inheritance mapping consists of two parts - discriminator mapping and subclasses mappings.

Discriminator column is mapped with Discriminator method. Mapping options offered by this method looks a bit messy. Length and NotNullable methods are available both in the Column method options (as in other mappings) and in the discriminator options directly. It looks like they are equivalent, what's called last "wins".

Mapping of subclasses is done through separate mapping classes that inherit from SubclassMapping<T>.

An important thing to remember when mapping inheritance using mapping-by-code is that when registering our mappings we should not look for ClassMapping<T> inheritors only - SubclassMapping<T> (and JoinedSubclassMapping<T> and UnionSubclassMapping<T>, mentioned later) do not inherit from ClassMapping<T>. The solution I'm using is to look for all implementations of IPropertyContainerMapper<T>.

Fluent NHibernate's equivalent for single table

Single table inheritance mapping is almost identical in Fluent NHibernate to mapping-by-code:

public class PartyMap : ClassMap<Party>
{
public PartyMap()
{
Id(x => x.Id).GeneratedBy.HiLo(maxLo: "100");

DiscriminateSubClassesOnColumn("discriminator")
.AlwaysSelectWithValue()
.Formula("arbitrary SQL expression")
.ReadOnly()
.Length(12)
.Not.Nullable()
.CustomType<string>();
}

public class CompanyMap : SubclassMap<Company>
{
public CompanyMap()
{
DiscriminatorValue("Company");
Map(x => x.CompanyName);
}
}

public class PersonMap : SubclassMap<Person>
{
public PersonMap()
{
DiscriminatorValue("Person");
Map(x => x.FirstName);
}
}
}

The way of defining subclasses is identical. The discriminator mapping differs in few names: Force is AlwaysSelectWithValue in FNH, Type is CustomType and Insert(false) can be mapped using ReadOnly. Moreover, the DiscriminateSubClassesOnColumn method, that is an entry point for discriminator mapping, requires specifying discriminator column name explicity - no default provided.

There are some more standard column-related methods available, analogously to mapping-by-code Column method options.

Table per class

The second strategy for mapping inheritance is table per class with joined subclasses. In this option subclasses are stored in separate tables that have foreign key to base class table and are joined with the table for base class, if needed. In this case, in mapping-by-code, we have to map subclasses by inheriting from JoinedSubclassMapping<T>. Here is the example of joined subclass mapping with all available options:

public class CompanyMap : JoinedSubclassMapping<Company>
{
public CompanyMap()
{
Key(k =>
{
k.Column("PartyId");
// or...
k.Column(c =>
{
c.Name("PartyId");
// etc.
});

k.ForeignKey("party_fk");
k.NotNullable(true);
k.OnDelete(OnDeleteAction.Cascade); // or OnDeleteAction.NoAction
k.PropertyRef(x => x.CompanyName);
k.Unique(true);
k.Update(true);
});

Property(x => x.CompanyName);
}
}

Fluent NHibernate's equivalent for table per class

In Fluent, subclasses are mapped by inheriting from the same SubclassMap<T> as in single table strategy. Only the method used differs - instead of DiscriminatorValue, we're using KeyColumn method with column name in the parameter (required, no default value). Looks like there's no way to configure other key column's options here.

public class CompanyMap : SubclassMap<Company>
{
public CompanyMap()
{
KeyColumn("PartyId");
Map(x => x.CompanyName);
}
}

Table per subclass

The third option is to use table per subclass with unioned subclasses. This time there is no separate table for base class, common columns are specified in each table for subclass separately and subclass tables share the same key generator. Mapping union subclasses with mapping-by-code is fairly simple - we just need to map the class by inheriting from UnionSubclassMapping<T> - no additional requirements or options.

public class CompanyMap : UnionSubclassMapping<Company>
{
public CompanyMap()
{
Property(x => x.CompanyName);
}
}

Fluent NHibernate's equivalent for table per subclass

Again, in Fluent, subclass maps inherit from the same SubclassMap<T>. To differentiate this strategy from the single table strategy, we have to include a call to UseUnionSubclassForInheritanceMapping method in the parent class mapping.

public class PartyMap : ClassMap<Party>
{
public PartyMap()
{
Id(x => x.Id).GeneratedBy.HiLo(maxLo: "100");
UseUnionSubclassForInheritanceMapping();
}

public class CompanyMap : SubclassMap<Company>
{
public CompanyMap()
{
Map(x => x.CompanyName);
}
}

public class PersonMap : SubclassMap<Person>
{
public PersonMap()
{
Map(x => x.FirstName);
}
}
}

29 comments:

  1. No matter what I do, UnionSubclassMapping creates a table for the base class also. Cannot figure out what am I doing wrong.

    ReplyDelete
    Replies
    1. So stupid of me. I was not marking the base class abstract. Fixed. Thanks anyways. :)

      Delete
    2. I try do this... put an abstract at base class declaration, and Abstract(true) at base class mapping... And UnionSubclassMapping creates a table for base class only...

      Delete
  2. Could you add an example using "8.1.3. Table per subclass, using a discriminator" (it's a reference to the manual)







    ...



    Thanks!

    ReplyDelete
  3. Propery rules does not work when using joinedsubclassmapping

    Property(p => p.Name, Rules.StrLength255AndNotNull);

    why?

    ReplyDelete
  4. Do you have a solution for a SubclassMap[B] where B inherits from A and A has a CompositeId() definition?

    ReplyDelete
  5. Have you tried mapping a subclass where you have multiple levels of inheritance? It does not seem to work on mapping by code, but regular xml mapping seems to be able to do this.

    Example:
    ClassC = subclass
    ClassA = baseclass

    ClassC inherits from ClassB inherits from ClassA

    ClassMap
    SubclassMap

    ReplyDelete
  6. How do we do "Table per concrete class, using implicit polymorphism" [http://www.nhforge.org/doc/nh/en/#inheritance-tableperconcreate-polymorphism] with mapping by code?

    ReplyDelete
    Replies
    1. Just like in XML - implicitly :) Map the derived classes only as if there were no inheritance. And ensure your ModelMapper (or your conventions) doesn't try to treat your base class as entity.

      Delete
    2. That's what I did :> and it almost works. I can query using linq and it works fine. But the problem is that if I use the Count() function from IQueryable, the value returned is wrong. It only returns the count from one of the derived classes. Like for instance if I have both Dog and Cat and they both implement IAnimal, if I query for animals I will get 10 animals (say 6 dogs and 4 cats) but if I get the animals.AsQueryable().Count() it will return only number 6 not 10. I can see in nhibernate profiler that it does two sql count queries, but I don't know what happens in nhibernate implementation that it does not sum the two counts up.

      That's why I suspected maybe I am doing something wrong in the mapping. Any ideas?

      Delete
    3. Well, looks like a bug in LINQ provider, definitely not in mappings.

      Delete
  7. Hey Adam,

    I have an inheritance chain like:
    class A {}
    class B : A {}
    class C : B {}

    None are abstract. Is it possible to map this using MBC?

    Thanks.

    ReplyDelete
    Replies
    1. Which approach do you want to take? What have you already tried? I've tried your hierarchy with all the four approaches and it works as expected in all cases.

      Delete
    2. * Correction. Table per class is the approach I'm wanting to take.

      Delete
    3. This appears to be an alphabetical naming issue. Everything works when B inherits from A, and C inherits from B. However, if you change C to inherit from A, and B to inherit from C, you'll see the same error I am getting.

      Delete
    4. Whoa, you're right, that's a pretty nasty bug! I'm able to reproduce it when my mapping order in the file is different than the inheritance order. When it's the same, it works fine. Ensure your ClassMaps are ordered correctly. And I'll look for JIRA bug entry on this one. Anyway, I'm not sure this is MbC issue.

      Delete
    5. Here it is - NH-2931 - known MbC issue, already marked as major prority. I'm pretty shocked that order matters :)

      Delete
    6. That's definitely it. Looks like the bug originates in MappingRootBinder.cs. The AddJoinedSubclasses method calls JoinedSubclassBinder.cs->Bind(), which in turn calls the GetSuperclass method where it ultimately fails. All JoinedSubclass objects should be added to the collection before that method is called.

      I'm going to sort the types prior to passing them into the ModelMapper->AddMappings method in the PostProcessMappings override method of our custom local session factory object (Spring.net). I'll let you know how it turns out.

      Thanks again Adam!

      Delete
  8. I'm wanting to use table per subclass. My results are just the opposite for each approach. My unit tests blow up with the message "Hibernate.MappingException: Could not compile the mapping document: mapping_by_code ---> NHibernate.MappingException: Cannot extend unmapped class: B".

    It's almost as if only one level of inheritance is supported.

    Thanks for your help.

    ReplyDelete
  9. I was able to get around the bug, but it was not straightforward whatsoever. Basically you have to order the HbmMapping items by their respective inheritance depth.

    //
    private IList<Type> _types = null;

    protected override void PostProcessMappings(Configuration Configuration)
    {base.PostProcessMappings(pConfiguration);
    _types = typeof(IEntity).Assembly.GetTypes();

    var mapper = new ModelMapper();
    mapper.AddMappings(_types);

    var hbmMapping = mapper.CompileMappingForAllExplicitlyAddedEntities();
    hbmMapping.Items = hbmMapping.Items.OrderBy(i => InheritanceHierarchyDepth((IEntityMapping)i)).ToArray<object>();

    pConfiguration.AddMapping(hbmMapping);
    _configuration = pConfiguration;
    AddForeignKeyDeleteActions();
    }

    private int InheritanceHierarchyDepth(IEntityMapping pMapping)
    {
    var type = _types.FirstOrDefault(t => t.FullName == pMapping.Name);
    var depth = 0;
    var parent = type;

    while (parent != null && parent != typeof(object))
    {
    parent = parent.BaseType;
    depth++;
    }

    return depth;
    }

    ReplyDelete
    Replies
    1. The method signature got messed up trying to format to pass the blog validation. Should be:

      protected override void PostProcessMappings(Configuration pConfiguration)
      {
      base.PostProcessMappings(pConfiguration);
      ........
      ........
      }

      Delete
  10. Hello Adam,
    Please I've been trying to use the Table per Class approach. However, I've been getting this error..

    Unable to save Entity of type : AvioCharge REASON::: could not insert: [Domains.AvioCharge][SQL: INSERT INTO Bills (ArrivalId, AmountCharged, DateOfTransaction, Airline, Approved, ChargeType, Currency) VALUES (?, ?, ?, ?, ?, ?, ?); select SCOPE_IDENTITY()]

    I believe it has something to do with the base class. You did not provide the base class mapping for the Table per Class approach. Is there something unique I might be missing out or can I use the same one as the Table per hierarchy approach.

    Thank you.

    ReplyDelete
    Replies
    1. And what is AvioCharge? The base class? What exactly is wrong with that SQL (check the inner exception)?

      Delete
    2. Are you really sure your Bill properties are fine? There must be something wrong if the SQL cannot be executed (mapping seems to be good, at first sight)

      Delete
  11. The Base Class is Bill.. AvioCharge inherits Bill.. On saving AvioCharge, I want it to save the Bill fields in the Bill table, then save the remaining fields in the AvioCharge Table..

    WHen this did not work, I even tried saving to the base class' table (Bill) using the Bill class itself. Still the same error.

    ReplyDelete
  12. [Serializable]
    public class Bill: BaseEntity
    {
    public virtual long ArrivalId { get; set; }
    public virtual decimal AmountCharged { get; set; }
    public virtual DateTime DateOfTransaction { get; set; }
    public virtual string Airline { get; set; }
    public virtual bool Approved { get; set; }
    public virtual int ChargeType { get; set; }
    public virtual string Currency { get; set; }
    }

    class BillMap : ClassMapping
    {
    public BillMap()
    {
    this.Table("Bills");
    this.Lazy(false);
    this.Id(x => x.Id, mp =>
    {
    mp.Column("Id");
    mp.Generator(Generators.Native);
    });


    this.Property(x => x.AmountCharged, mp => { mp.Column("AmountCharged"); });
    this.Property(x => x.ChargeType, mp => { mp.Column("ChargeType"); });
    this.Property(x => x.Approved, mp => { mp.Column("Approved"); });
    this.Property(x => x.Currency, mp => { mp.Column("Currency"); });
    this.Property(x => x.DateOfTransaction, mp => { mp.Column("DateOfTransaction"); });
    this.Property(x => x.Airline, mp => { mp.Column("Airline"); });
    this.Property(x => x.ArrivalId, mp => { mp.Column("ArrivalId"); });

    }
    }

    [Serializable]
    public class AvioCharge: Bill
    {
    public virtual string ATA { get; set; }
    public virtual DateTime EntranceDate { get; set; }
    public virtual decimal MTOW { get; set; }
    public virtual decimal ROE { get; set; }
    public virtual decimal AvioRate { get; set; }
    public virtual int Terminal { get; set; }
    public virtual bool IsScheduled { get; set; }
    }

    class AvioChargeMap : JoinedSubclassMapping
    {
    public AvioChargeMap()
    {
    Table("AvioCharge");
    this.Lazy(true);
    Key(x => { x.Column("BillId");
    x.ForeignKey("Id");
    x.NotNullable(true);
    x.Unique(true);
    x.Update(true);
    });

    this.Property(x => x.ATA, mp => { mp.Column("ATA"); });
    this.Property(x => x.EntranceDate, mp => { mp.Column("EntranceDate"); });
    this.Property(x => x.MTOW, mp => { mp.Column("MTOW"); });
    this.Property(x => x.ROE, mp => { mp.Column("ROE"); });
    this.Property(x => x.AvioRate, mp => { mp.Column("AvioRate"); });
    this.Property(x => x.IsScheduled, mp => { mp.Column("IsScheduled"); });
    this.Property(x => x.Terminal, mp => { mp.Column("Terminal"); });

    }
    }

    public void DoThis(AvioCharge av)
    {
    av= new AvioCharge();
    //populate av

    _session.SaveOrUpdate(av);
    }


    This triggers the error described above.

    ReplyDelete
  13. I Have a problem, I have a FieldA that is present in both classes (ClassA and ClassB)
    but in ClassA this FieldA is nullable, and in ClassB FieldA is not nullable, how to do that? Using Table per subclass doesn´t allow that, you have to map the Attribute in the Base class!

    ReplyDelete

Note: Only a member of this blog may post a comment.