Introduction
In this article, we'll dive into Inheritance Mapping with JPA and Hibernate in Java.
The Java Persistence API (JPA) is the persistence standard of the Java ecosystem. It allows us to map our domain model directly to the database structure and then gives us the flexibility of only manipulating objects in our code. This allows us not to dabble with cumbersome JDBC components like Connection
, ResultSet
, etc.
We'll be making a comprehensive guide to using JPA with Hibernate as its vendor. In this article, we'll be exploring inheritance mapping in Hibernate.
- Guide to JPA with Hibernate: Basic Mapping
- Guide to JPA with Hibernate: Relationship Mapping
- Guide to JPA with Hibernate: Inheritance Mapping (you're here!)
- Guide to JPA with Hibernate: Querying (coming soon!)
Inheritance Mapping
Basic mapping, such as mapping fields of an object or relationship mapping, where we map the relationship between different tables are super common, and you'll use these techniques in pretty much every application you're building. A bit more rarely, you'll map hierarchies of classes.
The idea here is to handle mapping of hierarchies of classes. JPA offers multiple strategies to achieve that, and we'll go through each of them:
- Mapped Superclass
- Single Table
- One Table Per (Concrete) Class
- Joined Table
Domain Model
First of all, let's add some inheritance in our domain model:
As we can see, we introduced the Person
class, which is a super class of both Teacher
and Student
and holds names and birth dates as well as address and gender.
In addition to that, we added the Vehicle
hierarchy to manage teacher's vehicles for parking management.
They can be Car
or Motorcycle
. Each vehicle has a license plate, but a car can run on LPG (which is forbidden on certain levels of the parking) and motorcycles can have a sidecar (which requires the parking space of a car).
Mapped Super-Class
Let's start with a simple one, the mapped superclass approach. A mapped superclass is a class that's not an entity but one that contains mappings. It's the same principle as embedded classes, but applied to inheritance.
So, let's say we want to map our new classes to handle teacher's parking in the school, we would first define the Vehicle
class, annotated with @MappedSuperclass
:
@MappedSuperclass
public class Vehicle {
@Id
private String licensePlate;
}
It only contains the identifier, annotated with @Id
, which is the license plate of the vehicle.
Now, we want to map our two entities: Car
and Motorcycle
. Both will extend from Vehicle
and inherit the licensePlate
:
@Entity
class Car extends Vehicle {
private boolean runOnLpg;
}
@Entity
class Motorcycle extends Vehicle {
private boolean hasSideCar;
}
Okay, we've defined our entities now, and they inherit from Vehicle
. Though, what happens on the database side? JPA generates these table definitions:
create table Car (licensePlate varchar(255) not null, runOnLpg boolean not null, primary key (licensePlate))
create table Motorcycle (licensePlate varchar(255) not null, hasSideCar boolean not null, primary key (licensePlate))
Each entity has its own table, both with a licensePlate
column, which is also the primary key of these tables. There is no Vehicle
table. The @MappedSuperclass
is not an entity. In fact, a class cannot have the @Entity
and @MappedSuperclass
annotations applied to it.
What are the consequences of Vehicle
not being an entity? Well, we can't search for a Vehicle
using the EntityManager
.
Let's add some cars and a motorcycle:
insert into CAR(LICENSEPLATE, RUNONLPG) values('1 - ABC - 123', '1');
insert into CAR(LICENSEPLATE, RUNONLPG) values('2 - BCD - 234', '0');
insert into MOTORCYCLE(LICENSEPLATE, HASSIDECAR) values('M - ABC - 123', '0');
Intuitively, you might want to search for a Vehicle
with the license plate 1 - ABC - 123
:
assertThrows(Exception.class, () -> entityManager.find(Vehicle.class, "1 - ABC - 123"));
And this will throw an exception. There are no persisted Vehicle
entities. There are persisted Car
entities though. Let's search for a Car
with that license plate:
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
Single Table Strategy
Let's now move on to the Single Table Strategy. This strategy allows us to map all the entities of a class hierarchy to the same database table.
If we reuse our parking example, that would mean cars and motorcycles would all be saved in a VEHICLE
table.
In order to set up this strategy we'll need a few new annotations to help us define this relationship:
@Inheritance
- which defines the inheritance strategy and is used for all strategies except for mapped superclasses.@DiscriminatorColumn
- which defines a column whose purpose will be to determine which entity is saved in a given database row. We'll mark this asTYPE
, denoting the vehicle type.@DiscriminatorValue
- which defines the value of the discriminator column for a given entity - so, whether this given entity is aCar
orMotorcycle
.
This time around, the Vehicle
is a managed JPA @Entity
, since we're saving it into a table. Let's also add the @Inheritance
and @DiscriminatorColumn
annotations to it:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "TYPE")
public class Vehicle {
@Id
private String licensePlate;
}
The @Inheritance
annotation accepts a strategy
flag, which we've set to InheritanceType.SINGLE_TABLE
. This lets JPA know that we've opted for the Single Table approach. This type is also the default type, so even if we hadn't specified any strategies, it'd still be SINGLE_TABLE
.
We also set our discriminator column name to be TYPE
(the default being DTYPE
). Now, when JPA generates a table, it'll look like:
create table Vehicle (TYPE varchar(31) not null, licensePlate varchar(255) not null, hasSideCar boolean, runOnLpg boolean, primary key (licensePlate))
That has a few consequences:
- Fields for both
Car
andMotorcycle
are stored in the same table, which can become messy if we have a lot of fields. - All subclass' fields have to be nullable (because a
Car
can't have values for aMotorcycle
field, and vice-versa), which means less validation on the database level.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
That being said, let's map our Car
and Motorcycle
now:
@Entity
@DiscriminatorValue("C")
class Car extends Vehicle {
private boolean runOnLpg;
}
@Entity
@DiscriminatorValue("M")
class Motorcycle extends Vehicle {
private boolean hasSideCar;
}
Here, we're defining the values of the discriminator column for our entities. We chose C
for cars and M
for motorcycles. By default, JPA uses the name of the entities. In our case, Car
and Motorcycle
, respectively.
Now, let's add some vehicles and take a look at how the EntityManager
deals with them:
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('1 - ABC - 123', 'C', '1', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('2 - BCD - 234', 'C', '0', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('M - ABC - 123', 'M', null, '0');
On one end, we can retrieve each Car
or Motorcycle
entity:
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
But, since Vehicle
is also an entity, we can also retrieve entities as their superclass - Vehicle
:
Vehicle foundCar = entityManager.find(Vehicle.class, "1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
In fact, we can even save a Vehicle
entity which is neither a Car
nor a Motorcycle
:
Vehicle vehicle = new Vehicle();
vehicle.setLicensePlate("T - ABC - 123");
entityManager.persist(vehicle);
Which translates into the following SQL query:
insert into Vehicle (TYPE, licensePlate) values ('Vehicle', ?)
Although we might not want that to happen - we must use the @Entity
annotation on Vehicle
with this strategy.
If you'd like to disable this feature, a simple option is to make the Vehicle
class abstract
, preventing anyone from instantiating it. If it's un-instantiable, it can't be saved as an entity, even though it's annotated as one.
One Table Per Class Strategy
The next strategy is called One Table Per Class, which, as the name implies, creates one table per class in the hierarchy.
Though, we could've used the term "Concrete Class" instead, since it doesn't create tables for abstract classes.
This approach looks a lot like the Mapped Superclass approach - the only difference being that the superclass is an entity as well.
To let JPA know we'd like to apply this strategy, we'll set the InheritanceType
to TABLE_PER_CLASS
in our @Inheritance
annotation:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
@Id
private String licensePlate;
}
Our Car
and Motorcycle
classes just have to be mapped using @Entity
and we're done. The table definitions are the same as with mapped superclass, plus a VEHICLE
table (because it's a concrete class).
But, what differs from mapped superclass is that we can search for a Vehicle
entity, as well as a Car
or Motorcycle
entity:
Vehicle foundVehicle = entityManager.find(Vehicle.class, "1 - ABC - 123");
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");
assertThat(foundVehicle).isNotNull();
assertThat(foundVehicle.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();
Joined Table Strategy
Finally, there's the Joined Table strategy. It creates one table per entity, and keeps each column where it naturally belongs.
Let's take our Person
/Student
/Teacher
hierarchy. If we implement it using the joined table strategy, we will end up with three tables:
create table Person (id bigint not null, city varchar(255), number varchar(255), street varchar(255), birthDate date, FIRST_NAME varchar(255), gender varchar(255), lastName varchar(255), primary key (id))
create table STUD (wantsNewsletter boolean not null, id bigint not null, primary key (id))
create table Teacher (id bigint not null, primary key (id))
The first one, PERSON
, gets the columns for all the fields in the Person
entity, while the others are only getting columns for their own fields, plus the id
that links the tables together.
When searching for a student, JPA will issue an SQL query with a join between STUD
and PERSON
tables in order to retrieve all the data of the student.
To map this hierarchy, we'll use the InheritanceType.JOINED
strategy, in the @Inheritance
annotation:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
@Id
@GeneratedValue
private Long id;
private String lastName;
@Column(name = "FIRST_NAME")
private String firstName;
private LocalDate birthDate;
@Enumerated(EnumType.STRING)
private Student.Gender gender;
@Embedded
private Address address;
}
Our other entities are just mapped using @Entity
:
@Entity
public class Student extends Person {
@Id
private Long id;
private boolean wantsNewsletter;
private Gender gender;
}
And:
@Entity
public class Teacher extends Person {
@Id
private Long id;
Let's also define the ENUM we've used in the Student
class:
enum GENDER {
MALE, FEMALE
}
There we go, we can fetch Person
, Student
and Teacher
entities as well as save them using EntityManager.persist()
.
Again, if we want to avoid creating Person
entities we must make it abstract
.
Conclusion
In this article, we dove into inheritance mapping using JPA and Hibernate and we've tackled a couple of different situations you might encounter.
The code for this series can be found over on GitHub.