JPA Inheritance Mapping Strategies – Single Table, Joined, Table per Class

Illustration for JPA Inheritance Mapping Strategies – Single Table, Joined, Table per Class
By Last updated:

Inheritance is a fundamental concept in object-oriented programming. In Java, classes can inherit fields and methods from their parent classes. But how do we map this inheritance hierarchy to relational databases using JPA (Java Persistence API)?

JPA provides multiple inheritance mapping strategies to represent class hierarchies in relational tables:

  1. Single Table Strategy (InheritanceType.SINGLE_TABLE)
  2. Joined Strategy (InheritanceType.JOINED)
  3. Table per Class Strategy (InheritanceType.TABLE_PER_CLASS)

Each strategy has trade-offs in performance, normalization, and schema design. In this tutorial, we’ll cover them with examples, SQL outputs, pros and cons, and best practices.


Setup and Configuration

persistence.xml

<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
    <persistence-unit name="my-pu" transaction-type="RESOURCE_LOCAL">
        <class>com.example.Employee</class>
        <class>com.example.FullTimeEmployee</class>
        <class>com.example.PartTimeEmployee</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Common Base Entity

import jakarta.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "employee_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

1. Single Table Strategy

Entities

@Entity
@DiscriminatorValue("FULL_TIME")
public class FullTimeEmployee extends Employee {
    private double salary;
}

@Entity
@DiscriminatorValue("PART_TIME")
public class PartTimeEmployee extends Employee {
    private double hourlyWage;
}

Generated Schema

create table employee (
    id bigint generated by default as identity,
    name varchar(255),
    salary double,
    hourlyWage double,
    employee_type varchar(31),
    primary key (id)
);

Pros

  • Fastest queries (single table).
  • Simple schema.

Cons

  • Many NULL columns when subclasses have distinct fields.
  • Schema less normalized.

2. Joined Strategy

Entities

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

@Entity
public class FullTimeEmployee extends Employee {
    private double salary;
}

@Entity
public class PartTimeEmployee extends Employee {
    private double hourlyWage;
}

Generated Schema

create table employee (
    id bigint generated by default as identity,
    name varchar(255),
    primary key (id)
);

create table full_time_employee (
    id bigint not null,
    salary double,
    primary key (id),
    foreign key (id) references employee
);

create table part_time_employee (
    id bigint not null,
    hourlyWage double,
    primary key (id),
    foreign key (id) references employee
);

Pros

  • Normalized schema.
  • No null columns.

Cons

  • Requires JOINs in queries → slower performance.

3. Table per Class Strategy

Entities

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
}

@Entity
public class FullTimeEmployee extends Employee {
    private double salary;
}

@Entity
public class PartTimeEmployee extends Employee {
    private double hourlyWage;
}

Generated Schema

create table full_time_employee (
    id bigint generated by default as identity,
    name varchar(255),
    salary double,
    primary key (id)
);

create table part_time_employee (
    id bigint generated by default as identity,
    name varchar(255),
    hourlyWage double,
    primary key (id)
);

Pros

  • Each subclass has its own table.
  • No null columns, no joins.

Cons

  • Duplicate columns across tables.
  • UNION queries needed for polymorphic retrieval.

CRUD Operations Example

EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-pu");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

tx.begin();
FullTimeEmployee fte = new FullTimeEmployee();
fte.setName("Alice");
fte.setSalary(80000);

PartTimeEmployee pte = new PartTimeEmployee();
pte.setName("Bob");
pte.setHourlyWage(30);

em.persist(fte);
em.persist(pte);
tx.commit();

Querying with JPA

JPQL

List<Employee> employees = em.createQuery("SELECT e FROM Employee e", Employee.class).getResultList();

Criteria API

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
Root<Employee> root = cq.from(Employee.class);
cq.select(root);
List<Employee> results = em.createQuery(cq).getResultList();

Persistence Context Considerations

  • Entities from inheritance hierarchies share the same persistence context.
  • Fetching strategy (LAZY vs EAGER) impacts performance, especially with Joined strategy.

Real-World Use Cases

  • Single Table: Small hierarchies with limited distinct fields.
  • Joined: Large hierarchies where normalization matters.
  • Table per Class: When each subclass is mostly independent.

Anti-Patterns and Pitfalls

  • Avoid Single Table for large hierarchies → too many nulls.
  • Avoid Table per Class when polymorphic queries are frequent.
  • Avoid Joined when performance is critical.

Best Practices

  • Choose mapping strategy based on query patterns and schema design.
  • Use JOINED for normalized schema, SINGLE_TABLE for performance.
  • Always benchmark with real data.

📌 JPA Version Notes

  • JPA 2.0: Introduced Criteria API and Metamodel.
  • JPA 2.1: Added entity graphs for fetching flexibility.
  • Jakarta Persistence: Migrated package from javax.persistence to jakarta.persistence.

Conclusion and Key Takeaways

  • JPA supports three inheritance strategies: SINGLE_TABLE, JOINED, TABLE_PER_CLASS.
  • Each has trade-offs in performance and schema design.
  • Always align strategy with real-world requirements.

By mastering inheritance mapping strategies, you’ll design cleaner, more efficient ORM-based applications.


FAQ

1. What’s the difference between JPA and Hibernate?
JPA is a specification, Hibernate is an implementation.

2. How does JPA handle the persistence context?
It manages entities and tracks changes across transactions.

3. What are the drawbacks of eager fetching in JPA?
Loads unnecessary data, causing performance issues.

4. How can I solve the N+1 select problem with JPA?
Use JOIN FETCH or entity graphs.

5. Can I use JPA without Hibernate?
Yes, alternatives include EclipseLink and OpenJPA.

6. What’s the best strategy for inheritance mapping in JPA?
JOINED for normalization, SINGLE_TABLE for performance.

7. How does JPA handle composite keys?
With @EmbeddedId or @IdClass.

8. What changes with Jakarta Persistence?
Package namespace changed to jakarta.persistence.

9. Is JPA suitable for microservices?
Yes, with DTOs and projections for lightweight communication.

10. When should I avoid using JPA?
For heavy batch processing or NoSQL-oriented systems.