Guide to JPA with Hibernate - Inheritance Mapping

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 as TYPE, denoting the vehicle type.
  • @DiscriminatorValue - which defines the value of the discriminator column for a given entity - so, whether this given entity is a Car or Motorcycle.

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 and Motorcycle 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 a Motorcycle field, and vice-versa), which means less validation on the database level.
Free eBook: Git Essentials

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.

Last Updated: October 3rd, 2023
Was this article helpful?

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms