It's common to have an inheritance hierarchy of subclasses. In this example, we will show you one method for mapping inheritance with NHibernate, called table-per-class hierarchy.
Book
with the following code:namespace Eg.Core { public class Book : Product { public virtual string ISBN { get; set; } public virtual string Author { get; set; } } }
Movie
with the following code:namespace Eg.Core { public class Movie : Product { public virtual string Director { get; set; } } }
Product
mapping to match the XML shown in the following code:<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <class name="Product"> <id name="Id"> <generator class="guid.comb" /> </id> <discriminator column="ProductType" /> <natural-id mutable="true"> <property name="Name" not-null="true" /> </natural-id> <property name="Description" /> <property name="UnitPrice" not-null="true" /> </class> </hibernate-mapping>
Boo
k.hbm.xml
with the following XML:<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <subclass name="Book" extends="Product"> <property name="Author"/> <property name="ISBN"/> </subclass> </hibernate-mapping>
Movie.hbm.xml
with the next XML:<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <subclass name="Movie" extends="Product"> <property name="Director" /> </subclass> </hibernate-mapping>
ClassHierarchy
in the MappingRecipes
project.Recipe
to the folder:public class Recipe : BaseMappingRecipe { protected override void Configure(Configuration cfg) { cfg.AddAssembly(typeof(Product).Assembly); } protected override void AddInitialData( ISession session) { session.Save(new Book { Name = "NHibernate Cookbook", ISBN = "12334" }); session.Save(new Movie { Name = "Intouchables", Director = "Olivier Nakache" }); } public override void RunQueries(ISession session) { session.CreateQuery("from Product") .List<Product>(); } }
MappingRecipes
application and start the ClassHierarchy
recipe.In this example, we've mapped a table-per-class hierarchy, which means that the data for our entire hierarchy is stored in a single table, as shown in the following screenshot:
NHibernate uses a discriminator column, ProductType
in this case, to distinguish among products, books, and movies. By default, the discriminator contains the class name. In this example, it would be Eg.Core.Product
, Eg.Core.Book
, or Eg.Core.Movie
. These defaults can be overridden in the mappings using a discriminator-value
attribute on our class and subclass elements.
In our Book.hbm.xml
mapping, we have defined Book
as a subclass of Product
with Author
and ISBN
properties. In our Movie.hbm.xml
mapping, we have defined Movie
as a subclass of Product
with a Director
property.
With table-per-class-hierarchy, we cannot define any of our subclass properties as not-null="true"
because this would create a not null constraint on those fields. For instance, if we set up the Director
property as not null, we would not be able to insert Product
or Book
instances, because they do not define a Director
property. If this is required, use one of the hierarchy mapping strategies that are listed in the next section.
Java refugees may recognize the extends
attribute, as extends
is the Java keyword used to declare class inheritance. NHibernate first came to life as a port of Java's Hibernate ORM.
Table-per-class hierarchy is the suggested method for mapping class hierarchies, but NHibernate always gives us other options. However, mixing these options within the same class hierarchy is discouraged and only works in very limited circumstances.
In table-per-class mappings, properties of the base class Product
are stored in a shared table, while each subclass gets its own table for the subclass properties.
Table per subclass uses the joined-subclass
element, which requires a key element to name the primary key column. As the name implies, NHibernate will use a join to query for this data. Also, notice that our Product
table doesn't contain a ProductType
column. Only table-per-class hierarchy uses discriminators. Using table-per-class, our Movie
mapping would appear as the following code:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <joined-subclass name="Movie" extends="Product"> <key column="Id" /> <property name="Director" /> </joined-subclass> </hibernate-mapping>
Executing our recipe with table per class mapping would create three tables and execute the following queries:
INSERT INTO Product(Name, Description, UnitPrice, Id) VALUES (@p0, @p1, @p2, @p3); INSERT INTO Book (Author, ISBN, Id) VALUES (@p0, @p1, @p2); INSERT INTO Product(Name, Description, UnitPrice, Id) VALUES (@p0, @p1, @p2, @p3); INSERT INTO Movie(Director, Id) VALUES (@p0, @p1);
SELECT product0_.Id AS Id0_, product0_.Name AS Name0_, product0_.Description AS Descript3_0_, product0_.UnitPrice AS UnitPrice0_, product0_1_.Author AS Author1_, product0_1_.ISBN AS ISBN1_, product0_2_.Director AS Director2_, CASE WHEN product0_1_.Id IS NOT NULL THEN 1 WHEN product0_2_.Id IS NOT NULL THEN 2 WHEN product0_.Id IS NOT NULL THEN 0 END AS clazz_ FROM Product product0_ LEFT OUTER JOIN Book product0_1_ ON product0_.Id=product0_1_.Id LEFT OUTER JOIN Movie product0_2_ ON product0_.Id=product0_2_.Id
Note how the extra tables are joined (LEFT OUTER
to be specific) and a CASE clause is used to return the clazz_
value, which indicates the return type to NHibernate.
In table-per-concrete-class mappings, each class gets its own table containing columns, for all properties of the class and the base class.
There is no duplication of data. That is, data from a book instance is written only in the Book
table, not the Product
table. To fetch Product
data, NHibernate will use the SQL UNION
operator to query all the three tables. Using table-per-concrete-class, our Movie
mapping would appear as shown in the following code:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <union-subclass name="Movie" extends="Product"> <property name="Director" /> </union-subclass> </hibernate-mapping>
Executing our recipe with table per concrete class mapping would result in the following queries:
INSERT INTO Book (Name, Description, UnitPrice, Author, ISBN, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5); INSERT INTO Movie (Name, Description, UnitPrice, Director, Id) VALUES (@p0, @p1, @p2, @p3, @p4);
SELECT product0_.Id AS Id0_, product0_.Name AS Name0_, product0_.Description AS Descript3_0_, product0_.UnitPrice AS UnitPrice0_, product0_.Author AS Author1_, product0_.ISBN AS ISBN1_, product0_.Director AS Director2_, product0_.clazz_ AS clazz_ FROM (SELECT [columns snipped…] 0 AS clazz_ FROM Product UNION SELECT [columns snipped…] 1 AS clazz_ FROM Book UNION SELECT [columns snipped…] 2 AS clazz_ FROM Movie ) product0_
52.15.42.128