There are a few interesting things about this few tables:
- There are two types of customers: Store and Individual
- Each customer is represented as a single record in Customer table and additionally single record in Individual or Store table. Customer has a CustomerType column based on which we can determine the type.
- Individual and Store tables have a primary key which is also a foreign key to the main Customer table. It means that Customer with ID 1231 will have corresponding record in Store (or Individual) table with the same ID.
The above tables can be also mapped without any fancy inheritance so question is why should we bother? The real reason for this hassle are business rules which are different for each type of customer (and potentially sales territory). To be more specific, there might be different rules for queuing deliveries, calculating discounts etc.
It makes sense to encapsulate those rules in a separate class for each type of consumer. This line of thinking leads us to the class diagram like this:
Customer is marked as a abstract class so it can't be instantiated. For simplicity I have added here only one method GetCurrentDiscount() which represents business logic specific for each type. Additionally StoreConsumer and IndividualConsumer classes have a Details property which "links" to two other tables - Store or Individual.
Inheritance Mapping
In general NHibernate supports three different inheritance mapping strategies. In this case I will use table per class hierarchy strategy. With Fluent NHibernate Customer class can be mapped like this:
1: public class CustomerMap : ClassMap<Customer>
2: {
3: public CustomerMap()
4: {
5: Id(x => x.Id).GeneratedBy.Native();
6:
7: Map(x => x.ModifiedDate).Not.Nullable();
8: Map(x => x.AccountNumber).ReadOnly().Unique();
9:
10: References(x => x.SalesTerritory, "TerritoryID");
11:
12: DiscriminateSubClassesOnColumn<string>("CustomerType")
13: .SubClass<IndividualCustomer>("I",
14: m => m.HasOne(x => x.Details)
15: .Cascade.SaveUpdate()
16: .FetchType.Join())
17: .SubClass<StoreCustomer>("S",
18: m => m.HasOne(x => x.Details)
19: .Cascade.SaveUpdate()
20: .FetchType.Join());
21: }
22: }
- Thanks to Id(...) and Map(...) methods we can tell Fluent NHibernate about identity column and other simple "data" columns. You can find more about basics here.
- References(...) method is used to map many-to-one association with SalesTerritory table. In this case column name doesn't follow the convention therefore it has to be explicitly specified.
- CustomerType column is used as a discriminator. "The discriminator column contains marker values that tell the persistence layer what subclass to instantiate for a particular row." CustomerType can have two values:
- I for individual customer, in which case IndividualCustomer class will be instantiated or
- S for store and for StoreCustomer class.
- Both subclasses map one additional one-to-one association. Based on above mapping you can't see that actually each subclass "links" to the different table, it will be clear though when you check both classes:
1: public class IndividualCustomer : Customer
2: {
3: public virtual Individual Details { get; set; }
4:
5: public override double GetCurrentDiscount()
6: {
7: // some logic here
8: }
9: }
1: public class StoreCustomer : Customer
2: {
3: public virtual Store Details { get; set; }
4:
5: public override double GetCurrentDiscount()
6: {
7: // some logic here
8: }
9: }
One-to-one association
There is one additional feature worth mentioning - as it was stated earlier, ID from Customer table equals ID from Store (or Individual) table. How it can be mapped to make sure that inserts will work? I will explain it based on Individual class:
1: public class Individual
2: {
3: public virtual int Id { get; set; }
4: public virtual int Contact { get; set; }
5: public virtual string Demographics { get; set; }
6: public virtual DateTime ModifiedDate { get; set; }
7: public virtual IndividualCustomer Customer { get; set; }
8: }
Here is the mapping class:
1: public class InidividualMap : ClassMap<Individual>
2: {
3: public InidividualMap()
4: {
5: Id(x => x.Id, "CustomerID").GeneratedBy.Foreign("Customer");
6:
7: Map(x => x.Demographics);
8: Map(x => x.ModifiedDate);
9: Map(x => x.Contact, "ContactID");
10:
11: HasOne(x => x.Customer).Constrained();
12: }
13: }
foreign - uses the identifier of another associated object. Usually used in conjunction with a one-to-one primary key association.With above mapping foreign generator knows about the Customer and will use exactly the same ID. More about foreign generator and different types of one-to-one association you can find here.
Related articles:
- Fluent NHibernate and Collections mapping
- James Gregory's post about Fluent NHibernate SubClass syntax changes
- Introduction to Fluent NHibernate
6 comments:
i just stumbled on this post.
Fantastic job!!! -- this is by far the best inheritance example i've ever seen.
thanks for sharing.
using a repository, how would i create a new individual customer?
Useful post. Can you post full source code, please?
Your blog post just helped me out solve a problem! Thanks for the clear explanation about how to use DiscriminateSubClassesOnColumn.
A very clarifying note.
It's been a long time since the posting and some things have changed. For instance the inline subclasses are obsolete. Now one should use SubclassMap instead.
Really interesting, thanks for sharing.
buy logo
Post a Comment