Inheritance is a common pattern in object-oriented programming, but it's not easily replicated in a database. This Java tip shows you how to model inheritance relationships in JPA with Hibernate.
Learn the pros and cons of four different ORM inheritance strategies and get tips for choosing the one that best supports your application needs.
The inheritance pattern in ORM
Inheritance is an object-oriented pattern in which one class extends (or specializes) another class in order to borrow (or inherit) some of its functionality. For example, let's say we have a class named Car
that represents all cars, including a make, model, and year. But now we want to create some more specialized types of car, such as SportsCar
and SportUtilityVehicle
. These cars would have all the features of the original Car
class, but with some additional functionality. A SportsCar
might need special parameters for designating speed. A SportsUtilityVehicle
might need flags for seating and towing capacity.
Databases have no notion of inheritance between entities, so JPA providers like Hibernate must provide special features for defining object-oriented inheritance relationships in the database.
JPA specifies four strategies for defining an inheritance relationship:
- Mapped Superclass
- Table Per Class
- Single Table
- Joined
We'll look at each of these strategies as they're implemented by Hibernate.
Inheritance strategy #1: Mapped Superclass
The Mapped Superclass is the simplest inheritance strategy. It allows you to define common attributes in a base class, then extend that superclass in your other classes. Here's an example based on our Car
class:
@MappedSuperclass
public abstract class Car {
@Id
@GeneratedValue
private Integer id;
private String make;
private String model;
private Integer year;
...
}
The Car
class is annotated with the @MappedSuperclass
annotation and defines common car attributes, such as the make, model, and year, as well as the autogenerated id
of the car.
Now let's see what happens when we define a SportsCar
class and SportUtilityVehicle
class extending Car
:
@Entity
@Table(name = "SPORTSCAR")
public class SportsCar extends Car {
private Integer topSpeed;
private Double zeroToSixty;
...
}
@Entity
@Table(name = "SUV")
public class SportsUtilityVehicle extends Car {
private Integer towingCapacity;
private Boolean thirdRowSeating;
...
}
The Mapped Superclass strategy is simple but limited, in that both of these classes will map to tables that contain all of the car attributes: both the original Car
attributes and new, specialized class attributes.
Using this pattern means Hibernate won't be able to run polymorphic queries against individual cars. If we define a CarDealer
class that manages different types of cars, there's no way to maintain a list of Cars
in the CarDealer
; instead we would need to create a list of SportsCar
s and a list of SportUtilityVehicle
s.
We might do better with a slightly more complex inheritance strategy.
Inheritance strategy #2: Table Per Class
The next strategy for implementing inheritance is called Table Per Class. This strategy stores all data in its specialized class tables, just like we did with the mapped superclass, but it maintains the relationships between specialized classes and their base class.
In this case, the specialized classes, SportsCar
and SportsUtilityVehicle
, stay the same, but the base class is treated as an entity.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Car {
@Id
@GeneratedValue
private Integer id;
private String make;
private String model;
private Integer year;
...
}
We define the base class as an entity, using the @Entity
annotation. We then introduce the @Inheritance
annotation, specifying the InstanceType.TABLE_PER_CLASS
strategy.
With this strategy, we're able to execute polymorphic queries, so we can map a list of Car
s to a CarDealer
.
A complicated union
While Table Per Class provides the functionality we're looking for, its generated queries are complex. There are also potential performance issues with this strategy, because we have to perform a union of all the Car
subclass tables, then resolve the result set from that.
As an example, say we were to add two cars to our application code: one SUV and one sports car. The resultant database tables would contain the following:
Table: CAR
Table: SPORTSCAR
{ID: 1, MAKE: Carrera, MODEL: Porche, YEAR: 2018, TOPSPEED: 150, ZEROTOSIXTY: 4.5}
Table: SUV
{ID: 2, MAKE: CX-9, MODEL: Mazda, YEAR: 2017, THIRDROW: true, TOWINGCAPACITY: 3000}
Inheritance strategy #3: Single Table
The next strategy maps all fields from all entities to a single table, with a discriminator column to identify the instance type:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "Car_Type")
public abstract class Car {
@Id
@GeneratedValue
private Integer id;
private String make;
private String model;
private Integer year;
...
}
@Entity
@Table(name = "SPORTSCAR")
@DiscriminatorValue("SportsCar")
public class SportsCar extends Car {
private Integer topSpeed;
private Double zeroToSixty;
...
}
@Entity
@Table(name = "SUV")
@DiscriminatorValue("SUV")
public class SportsUtilityVehicle extends Car {
private Integer towingCapacity;
private Boolean thirdRowSeating;
...
}
The single table strategy is implemented using the @Inheritance
annotation with InstanceType.SINGLE_TABLE
. We add to this a @DiscriminatorColumn
annotation, which defines a column name we'll use to discriminate between specific class types. The specialized classes, such as SportsCar
and SportsUtilityVehicle
, extend the base class and add a @DiscriminatorValue
annotation, which specifies the name to identify its instances in the database.
The challenge with this implementation is that each table maintains all fields for all attributes for all specialized classes. You also lose the ability to define specialized fields to be non-null, because they will be null for other specialized classes. For example, a row representing a SportsCar
will have topSpeed
and zeroToSixty
values, but will have null towingCapacity
and thirdRowSeating
column values.
Based on our implementation of this pattern, the contents of the database would be as follows:
Table: CAR
{CAR_TYPE: SportsCar, ID: 1, MAKE: Carrera, MODEL: Porche, YEAR: 2018, TOPSPEED: 150, ZEROTOSIXTY: 4.5, THIRDROW: null, TOWINGCAPACITY: null},
{CAR_TYPE: SUV, ID: 2, MAKE: CX-9, MODEL: Mazda, YEAR: 2017, TOPSPEED: null, ZEROTOSIXTY: null, THIRDROW: true, TOWINGCAPACITY: 3000},
Note that both cars are stored in the CAR
table and each table [has a] CAR_TYPE
attribute (from the @DiscriminatorColumn
) that tells Hibernate the type of car (from the @DiscriminatorValue
.) Also note that each row contains all column values, with the unused ones being set to null
.
This strategy allows for efficient and polymorphic queries. On the downside, it could require maintaining many null and unnecessary columns.
Inheritance strategy #4: Joined
The final JPA inheritance strategy separates data between the base class and specialized classes into their own tables. The base class table contains all of the common attributes defined in the base class, while the specialized class tables contain only their specific attributes. We accomplish this separation by using the InstanceType.JOINED
inheritance strategy:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Car {
@Id
@GeneratedValue
private Integer id;
private String make;
private String model;
private Integer year;
...
}
The effect of this type of inheritance is probably what you most expect when defining an inheritance relationship. For our example, the database contains the following values:
Table: CAR
{ID: 1, MAKE: Carrera, MODEL: Porche, YEAR: 2018}
{ID: 2, MAKE: CX-9, MODEL: Mazda, YEAR: 2017}
Table: SPORTSCAR
{TOPSPEED: 150, ZEROTOSIXTY: 4.5, ID: 1}
Table: SUV
{THIRDROW: true, TOWINGCAPACITY: 3000, ID: 2}
Using this strategy results in tables that don't have a lot of null
values, as they did with the SINGLE_TABLE
strategy. You also don't have to write the full set of table unions that the TABLE_PER_CLASS
requires. On the downside, Hibernate has to perform a join between the base class table and the specialized class tables, which makes queries considerably more complex.
Choosing a JPA inheritance strategy
With four choices, the decision of which strategy to use depends on your use case. In general, I recommend avoiding the TABLE_PER_CLASS
strategy because of the query overhead. I also suggest avoiding the MAPPED_SUPERCLASS
strategy--or using it only for cases where you don't need to use your classes polymorphically.
That leaves the SINGLE_TABLE
and JOINED
strategies.
- If you need fast queries and don't mind maintaining extra, unused
null
columns in a single table, then go with theSINGLE_TABLE
strategy. - If you don't want to maintain unused columns and you want to model your inheritance relationships more like you would when thinking about a database model, then use the
JOINED
strategy.
Personally, I almost always use JOINED
for inheritance relationships in JPA.
This story, "Inheritance relationships in JPA and Hibernate" was originally published by JavaWorld.