Hibernate Inheritance Mapping Example
In this post, we feature a comprehensive article about Hibernate Inheritance Mapping.
Inheritance is one of the fundamental design principles o Object-Oriented Languages. In Java, it is very common to have inheritance implemented in domain models. However, relational databases don’t support inheritance and they support flat structure.
Java Persistence API suggests different strategies to support inheritance hierarchies. In this article, we are going to study how to hibernate implements these strategies and how to map inheritance hierarchies.
1. What are we trying to solve (Domain model)?
We are trying to solve the below scenario in hibernate,
- Representing employees in an organization
- Employees can be classified as Contract Employees and Permanent Employees
- General employee attributes are defined in
Employee
superclass ContractEmployee
andPermanentEmployee
are subclasses and they have more specific attributes
Class diagram of our entity classes is as below,
2. Hibernate Inheritance strategies
2.1 Single Table Strategy
This is typically the best inheritance strategy. In this strategy, whole inheritance hierarchy’s data is stored in a single table. A discriminator column is used to determine to which class the row belongs.
All the Contract and Permanent employee details are stored in the employee table and they are differentiated by discriminator column employee_type. Below is the domain class structure,
Single Table Strategy entity classes
@Entity @Table(name = "EMPLOYEE") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "EMPLOYMENT_TYPE") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class Employee { @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Column(name = "EMPLOYEE_ID") private String employeeId; private String firstName; private String lastName; private String email; } @Entity @DiscriminatorValue("CONTRACT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class ContractEmployee extends Employee { private LocalDate contractStartDate; private LocalDate contractEndDate; private String agencyName; } @Entity @DiscriminatorValue("PERMANENT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class PermanentEmployee extends Employee { private LocalDate startingDate; private String costCenter; private Float totalLeaves; }
As you see only the parent class has a database table associated with it and hibernate automatically creates a discriminatory column employee_type for us.
Implementing this strategy becomes quite tricky when the child table has nullable values and also if you are implementing an ORM solution to an existing database schema as there may not be any discriminator column.
2.2 Table per Class Strategy
In table per class strategy, each concrete class has an associated database table and stores all the attributes in the class hierarchy to store all the attributes of that class and its parent class.
In table per class hierarchy, each class in the hierarchy has a mapping database table. Contract employee details are stored in table_per_class_contract_employee and permanent employee details are stored in table_per_class_permanent_employee table respectively. Since they are stored in different tables, there is no need to have a discriminator column. Entity class structure is as below,
Table per Class Strategy entity classes
@Entity(name = "tablePerClassEmployee") @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class Employee { @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Column(name = "EMPLOYEE_ID") private String employeeId; private String firstName; private String lastName; private String email; } @Entity(name = "tablePerClassContractEmployee") @Table(name = "TABLE_PER_CLASS_CONTRACT_EMPLOYEE") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class ContractEmployee extends Employee { private LocalDate contractStartDate; private LocalDate contractEndDate; private String agencyName; } @Entity(name = "tablePerClassPermanentEmployee") @Table(name = "TABLE_PER_CLASS_PERMANENT_EMPLOYEE") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class PermanentEmployee extends Employee { private LocalDate startingDate; private String costCenter; private Float totalLeaves; }
Less performant as a result of additional joins. Some JPA providers even don’t support this strategy. Sometimes ordering is quite tricky as ordering is done based on the class and later by the ordering criteria.
2.3 Mapped Super Class strategy
It is like table per class strategy but it does not allow querying, persisting or any relationships to parent class’s table. The mapped superclass should be an abstract class and isn’t marked with @Entity annotation.
In mapped superclass strategy parent class isn’t mapped to any database table. However, the database table mapping to child class contains all the attributes of the parent class. Below is the code for entity classes,
Mapped Superclass Strategy entity classes
@MappedSuperclass @DiscriminatorColumn(name = "EMPLOYMENT_TYPE") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class Employee { @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Column(name = "EMPLOYEE_ID") private String employeeId; private String firstName; private String lastName; private String email; } @Entity(name = "mappedSuperClassContractEmployee") @Table(name = "MAPPED_CONTRACT_EMPLOYEE") @PrimaryKeyJoinColumn(name = "ID") @DiscriminatorValue("CONTRACT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class ContractEmployee extends Employee { private LocalDate contractStartDate; private LocalDate contractEndDate; private String agencyName; } @Entity(name = "mappedSuperClassPermanentEmployee") @Table(name = "MAPPED_PERMANENT_EMPLOYEE") @PrimaryKeyJoinColumn(name = "ID") @DiscriminatorValue("PERMANENT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class PermanentEmployee extends Employee { private LocalDate startingDate; private String costCenter; private Float totalLeaves; }
The subclass cannot omit the superclass’s attributes. This makes the classes tightly coupled. Also, you cannot have any relationships to the mapped superclass, they can’t be queried or persisted separately.
2.4 Joined Table Strategy
This is the most logical solution, as it mirrors the object structure in the database. In this approach, a separate database table is defined for each of the class in the hierarchy and each table stores only its local attributes. Along with attribute, each table should have an id column and the id is defined in the parent table.
Each of the entity classes is associated with a database table. Additionally, the parent class’s mapped table contains a discriminatory column. In our demo, join_table_employee contains the employee_type discriminatory column. Below is the entity structure,
Joined Table Strategy entity classes
@Entity(name = "joinedTableEmployee") @Table(name = "JOIN_TABLE_EMPLOYEE") @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "EMPLOYMENT_TYPE") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class Employee { @Id @GeneratedValue( strategy = GenerationType.AUTO ) private Long id; @Column(name = "EMPLOYEE_ID") private String employeeId; private String firstName; private String lastName; private String email; } @Entity(name = "joinedTableContractEmployee") @Table(name = "JOIN_TABLE_CONTRACT_EMPLOYEE") @PrimaryKeyJoinColumn(name = "ID") @DiscriminatorValue("CONTRACT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class ContractEmployee extends Employee { private LocalDate contractStartDate; private LocalDate contractEndDate; private String agencyName; } @Entity(name = "joinedTablePermanentEmployee") @Table(name = "JOIN_TABLE_PERMANENT_EMPLOYEE") @PrimaryKeyJoinColumn(name = "ID") @DiscriminatorValue("PERMANENT") @SuperBuilder @NoArgsConstructor @AllArgsConstructor public class PermanentEmployee extends Employee { private LocalDate startingDate; private String costCenter; private Float totalLeaves; }
Generally, this approach is less performant as a result of unnecessary joins. Additionally, if you are migrating from an existing database, the discriminator column may be missing. Sometimes we may not need a table for every subclass.
3. Example
Refer this article to configure hibernate in your project.
In this example, I am using the Java-based configuration. The project is built using Java 11, Hibernate 5.x and Intellij Idea editor. For the database, I am using PostgreSQL.
You need to install a project in Lombok’s plugin. Lombok annotations help to keep the code clean and can reduce many boilerplate codes like getters and setters, all args and no-args constructors.
How to run the demo application?
- Download the source code and import it as a Maven project in IntelliJ idea
- All the entities are present in com.jcg.entity package and main classes for each of the strategies are present under com.jcg.demo package
- Hibernate’s table generation strategy is set to `create-drop`. Each time you run a program it recreates the table and previously saved data is lost
- To see the demo working run each of the main class separately and after the successful run check the respective database tables
Further enhancements which are worth to try are as follows,
- Change the settings of
HBM2DDL_AUTO
` inHibernateUtil
to see how the programs behave - Try with package-level mapping instead of mapping individual classes
4. Download the Source code
You can download the full source code of this example here: Hibernate Inheritance Mapping Example
Mapped Superclass is not mandatory to be abstract as per the official hibernate docs.
Reference:
https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#entity-inheritance-mapped-superclass
Mapped superclass strategy doesn’t need @DiscriminatorCloumn & @DiscriminatorValue