Inheritance Mapping
In no other place in J2EE development does the divide between application design and database persistence become quite so pronounced as it does when we attempt to map an inheritance hierarchy to a relational database. Inheritance mapping is a difficult thing to do, because databases don't really have any concept that is equivalent to inheritance, while at the same time, inheritance is a central part of every object-oriented domain model.
There are a number of different ways to map inheritance hierarchies to a database, with some options being better than others, and each option having its own set of drawbacks.
This tutorial will explore and contrast the three key mechanisms for mapping an object-oriented, inheritance hierarchy, into a relational database and its tables.
The Inheritance Model
To demonstrate inheritance mapping, I'm going to use a very simple hierarchy of three classes, with the Ancestor class being at the top, the Parent class being in the middle, and the Child class being at the bottom.
The Ancestor-Parent-Child Class Diagram
The Ancestor class will declare a property of type id, along with a nationality property of type String:
package com.examscam.mappings;
import javax.persistence.*;
@Entity
public class Ancestor {
private Long id;
private String nationality;
@Id
@GeneratedValue
public Long getId() {return id;}
public void setId(Long id) {this.id = id;}
public String getNationality() {return nationality;}
public void setNationality(String nationality) {
this.nationality = nationality;
}
}
The Parent class inherits all of the properties of the Ancestor, and declares a new String property called lastName:
package com.examscam.mappings;
import javax.persistence.*;
@Entity
public class Parentextends Ancestor{
private String lastName;
public String getLastName() {return lastName;}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
The Child class will extend the Parent class, thus inheriting all of the properties defined in both the Parent and Ancestor classes. Furthermore, the Child class will declare one new String property called firstName:
package com.examscam.mappings;
import javax.persistence.*;
@Entity
public class Child extends Parent {
private String firstName;
public String getFirstName() {return firstName;}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}Inheritance Strategies
Hibernate provides three separate options for mapping inheritance hierarchies:
- TABLE_PER_CLASS
- JOINED
- SINGLE_TABLE (the default)
To specify a strategy, you simply place the @Inheritance annotation after the @Entity tag on
the class at the top of your class hierarchy, which in this case, would be the Ancestor class. From there, you specify the strategy attribute, which can be set to any one of the three values the InheritanceType enum can take. So, to specify a TABLE_PER_CLASS inheritance strategy, the Ancestor class declaration would look like this:
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Ancestor { }Using the TABLE_PER_CLASS inheritance strategy, Hibernate expects three separate tables to be defined in the database, with each table containing the aggregate of all of the declared and inherited properties of the associated class. As a result, the ancestor table would only have two properties, namely the id and nationality fields, whereas the child table duplicates these properties, adds in columns for the Parent's lastName property, and then adds fields for the locally declared firstName property.
 Joined Table Inheritance
One of the ugly aspects of the TABLE_PER_CLASS inheritance type is the fact that properties get duplicated down the class hierarchy. For example, why should the child table declare columns of type id and nationality when they are already defined in the ancestor table? The JOINED inheritance type addresses this problem by having tables only maintain data that maps directly to the properties in the associated class. Subclasses are then linked to their inherited properties through common primary key fields in the tables, linking as UNION joins at runtime.
To use a JOIN for mapping inheritance, all we have to do is edit the @Inheritance annotation of the Ancestor class, and set the strategy attribute to InheritanceType.JOINED:
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public class Ancestor { ... }
When using a JOINED strategy for our problem domain, Hibernate expects three database tables, with each table mapping to the various properties defined in each class. The id field then forms a union between tables. So, if an Ancestor instance is saved, a record will be created in only the ancestor table, whereas if a Child instance is created, the fields of the instance will be saved in all three tables, with the records in each table associated, or joined, through a common value for the id field. In essence, the class hierarchy of an instance is JOINED together from all of the individual tables.
The Single Table Inheritance Strategy
So, one of the problems with the TABLE_PER_CLASS inheritance strategy was that sub-classes re-defined properties in their corresponding database tables that were already defined in tables that mapped to parent classes. The JOINED table strategy addresses this deficiency by having the database tables that map to classes only define columns that map to properties for that particular class. A join between the primary key column of tables making up an object's class hierarchy makes it possible to re-create a given entity at runtime. However, the JOINED strategy does have its own set of problems, not the least of which is its inefficiency.
Imagine you wanted to get a list of all of the Child objects? In that case, the database would have to issue a select * statement against the child, parent and ancestor tables, as properties of a Child instance are spread out over all three tables. This isn't an efficient process at all. Furthermore, three classes tightly joined together by a common primary key, mapped between tables with a one-to-one relationship, could just as easily be flattened out into a single table without any complex primary key joins at all. That is exactly what the third, and often preferential method for mapping class hierarchies, utilizes. The third and final strategy for mapping inheritance is the SINGLE_TABLE strategy.
For our problem domain, in order to use the SINGLE_TABLE strategy, we must change the strategy attribute of the @Inheritance annotation to the value of InheritanceType.SINGLE_TABLE.
With this inheritance strategy, all of the properties on the entire class hierarchy are defined in a single table, along with an extra field, named DTYPE, that keeps track of the Java class type associated with the given record.
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
public class
Ancestor { }
Benefits of the Single Table
Inheritance Type
The single table inheritance type is often the best choice for performing inheritance mapping. First of all, it minimizes the number of tables that need to be queried, which can be significant for large class hierarchies. Furthermore, mapping associations can be very difficult with other strategies.
For example, imagine we were using a
single class per table strategy, and we decided an Ancestor could be
associated in a one to one manner with another type, say a Location
type. Since this relationship would be true for all subclasses, and
given the fact that associations are implemented at the database level
by foreign keys, we would have to add a new foreign key column to
every table that was based on a sub-class of Ancestor. In a large
class hierarchy, adding those foreign key fields would be extremely
onerous and error prone. On the other hand, with the single table inheritance
strategy, only one column would need to be added to one table, which
would be a much, much simpler process.
Testing the
InheritanceType.SINGLE_TABLE
Here's a little main method you can use to test the single table inheritance type with the Ancestor, Parent and Child classes:
public static void main(String args[]) {
Ancestor a = new Ancestor();
a.setNationality("Korean");
Parent p = new Parent();
p.setNationality("Jewish"); p.setLastName("Steinberg");
Child c = new Child();
c.setNationality("Irish");
c.setLastName("McKenzie");
c.setFirstName("Cameron");
Session session = HibernateUtil.beginTransaction();
session.save(a); session.save(p); session.save(c);
HibernateUtil.commitTransaction();
}
Saving an Inheritance Hierarchy
When we run the main method that saves an instance of an Ancestor, Parent and Child class, we see the following output in the Hibernate log files:
Hibernate:
insert into Ancestor
(nationality, DTYPE) values (?, 'Ancestor')
Hibernate:
insert into Ancestor (nationality, lastName, DTYPE) values
(?, ?, 'Parent')
Hibernate:
insert into Ancestor (nationality, lastName, firstName,
DTYPE) values (?, ?, ?, 'Child')
As you can see, each record gets saved into the table named after the top of the class hierarchy, which in this case is Ancestor. However, in the actually SQL, Hibernate hard-codes the class type, be it Ancestor, Parent or Child, into the SQL statement, so that the DTYPE field can be used to positively identify the Java type the given records is associated with. So, to get all records associated with the Ancestor class, you would perform a single query against the Ancestor table for records where the DTYPE is Ancestor, Parent or Child, as all share an is-a relationship with the Ancestor class. On the other hand, to find all records associated with Child instances, Hibernate would simply query the Ancestor table for records where the DTYPE is set to Child. Overall, this data access strategy tends to be fairly simple, fairly efficient, and the most manageable.
 Looking at the database after running our main method, we can see three records, all differentiated by the DTYPE column, as being associated with either the Ancestor, Child or Parent class.
|