Hibernate Table Per Sub

Hibernate Table Per Subclass Inheritance with JOINED (2026) | D.Tech Academic

Hibernate Table Per Subclass Inheritance with JOINED (2026)

Last Updated: | Author: Deep Singh

When your domain model has an inheritance hierarchy — say a Person base class with Employee and Student subclasses — you need to decide how that hierarchy maps to database tables. Hibernate supports three strategies, and the one we'll look at here is table per subclass, activated by InheritanceType.JOINED.

In this tutorial, you'll build a small Person/Employee/Student model and save one of each. Along the way, you'll see exactly what tables Hibernate creates, how the JOINs work, and why this strategy is usually the best default for production systems.

The Three Inheritance Strategies

Before we jump into code, here's a quick comparison of the three strategies Hibernate supports:

  • SINGLE_TABLE — one table for the whole hierarchy, with a discriminator column. Fast reads, but no NOT NULL constraints on subclass columns.
  • JOINED (table per subclass) — a table per class. Shared fields go into the parent table; subclass-specific fields go into their own tables, linked by primary key. Normalized, supports NOT NULL, but requires JOINs for queries.
  • TABLE_PER_CLASS — a fully independent table per concrete class, with duplicated columns. Rarely used; polymorphic queries become expensive UNIONs.

In this tutorial we're using JOINED. It's the most normalized of the three and works well when you need foreign keys pointing to the base type.

Maven Dependencies

Add the following dependencies to your pom.xml inside the <dependencies> block.

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.6.15.Final</version>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.4.0</version>
    </dependency>
</dependencies>

Note on versions: The original tutorial used mysql:mysql-connector-java:5.1.6, which is 15 years old. The MySQL connector has since moved to the group com.mysql and the artifact mysql-connector-j. The driver class name changed too, from com.mysql.jdbc.Driver to com.mysql.cj.jdbc.Driver.

hibernate.cfg.xml Configuration

Place this file in src/main/resources.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">
            com.mysql.cj.jdbc.Driver
        </property>
        <property name="hibernate.connection.url">
            jdbc:mysql://localhost:3306/stds?useSSL=false&serverTimezone=UTC
        </property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>

        <property name="hibernate.dialect">
            org.hibernate.dialect.MySQL8Dialect
        </property>
        <property name="hibernate.hbm2ddl.auto">update</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
    </session-factory>
</hibernate-configuration>

The Entity Classes

Person.java — The Base Class

package com.deepsingh44.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "person")
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

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

    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

What's important here:

  • @Inheritance(strategy = InheritanceType.JOINED) — tells Hibernate to use table per subclass.
  • @Table(name = "person") — explicitly names the parent table. The original used "tph" (which stands for "table per hierarchy" — the wrong abbreviation for a JOINED strategy). "person" is clearer.
  • @GeneratedValue(strategy = GenerationType.IDENTITY) — the original used AUTO, which works but may choose an unexpected generation strategy depending on the database. IDENTITY is explicit and matches MySQL's AUTO_INCREMENT.

Employee.java — Subclass

package com.deepsingh44.model;

import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;

@Entity
@PrimaryKeyJoinColumn(name = "id")
public class Employee extends Person {

    private float salary;

    public float getSalary() {
        return salary;
    }

    public void setSalary(float salary) {
        this.salary = salary;
    }
}

Student.java — Subclass

package com.deepsingh44.model;

import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;

@Entity
@PrimaryKeyJoinColumn(name = "id")
public class Student extends Person {

    private float marks;

    public float getMarks() {
        return marks;
    }

    public void setMarks(float marks) {
        this.marks = marks;
    }
}

Why @PrimaryKeyJoinColumn? In the JOINED strategy, each subclass table has its own primary key that's also a foreign key to the parent table. @PrimaryKeyJoinColumn(name = "id") tells Hibernate which column name to use for that shared key. Without it, Hibernate falls back to the default name id, which works here, but being explicit is good practice — especially if you ever rename the parent column.

Note: The original tutorial didn't include @PrimaryKeyJoinColumn. It worked because Hibernate defaulted to the field name, but the code was relying on an implicit convention rather than declaring intent.

Saving Records — Test.java

package com.deepsingh44.model;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class Test {
    public static void main(String[] args) {

        StandardServiceRegistry ssr = new StandardServiceRegistryBuilder()
                .configure("hibernate.cfg.xml")
                .build();

        Metadata meta = new MetadataSources(ssr)
                .addAnnotatedClass(Person.class)
                .addAnnotatedClass(Employee.class)
                .addAnnotatedClass(Student.class)
                .getMetadataBuilder()
                .build();

        SessionFactory factory = meta.getSessionFactoryBuilder().build();
        Session session = factory.openSession();
        Transaction tx = session.beginTransaction();

        try {
            Person p = new Person();
            p.setName("Mala");

            Employee emp = new Employee();
            emp.setName("Sujeet");
            emp.setSalary(50000);

            Student s = new Student();
            s.setName("Rohit");
            s.setMarks(78.8f);

            session.save(p);
            session.save(emp);
            session.save(s);

            tx.commit();
            System.out.println("Saved successfully");
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            session.close();
            factory.close();
        }
    }
}

What's fixed here:

  • Transaction opened before saving. The original called session.save() three times before session.beginTransaction().commit() — the order was wrong. It worked by accident because Hibernate auto-began on the first save, but that's fragile. Now the transaction is opened first.
  • Try/catch/finally. Ensures rollback on failure and proper cleanup.
  • Session and factory closed. The original never closed either. In a short-lived test this doesn't matter, but the habit of leaving connections open is a leading cause of production bugs.

How the Tables Are Created

With hbm2ddl.auto=update, Hibernate creates three tables when the app starts:

CREATE TABLE person (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    PRIMARY KEY (id)
);

CREATE TABLE employee (
    id INT NOT NULL,
    salary FLOAT,
    PRIMARY KEY (id),
    FOREIGN KEY (id) REFERENCES person(id)
);

CREATE TABLE student (
    id INT NOT NULL,
    marks FLOAT,
    PRIMARY KEY (id),
    FOREIGN KEY (id) REFERENCES person(id)
);

Notice the structure:

  • Shared fields (name) live only in person.
  • Subclass-specific fields (salary, marks) live in their own tables.
  • Each subclass table's primary key is a foreign key back to person. That's what "table per subclass" means — the identifier is shared, not duplicated.

What Hibernate Does When You Save

When you call session.save(emp):

  1. Hibernate inserts a row into person with the name.
  2. It reads back the generated ID.
  3. It inserts a row into employee using that same ID as the primary key.

For a plain Person, only the person table gets a row. For a Student, both person and student get rows.

What a Query Looks Like

When you load an Employee by ID, Hibernate generates a JOIN:

SELECT e.id, p.name, e.salary
FROM employee e
INNER JOIN person p ON e.id = p.id
WHERE e.id = ?;

This is the trade-off with JOINED: you get a clean, normalized schema, but every read requires a JOIN. For most applications this is a fine trade-off. For extremely read-heavy workloads where the JOIN becomes a bottleneck, SINGLE_TABLE can be more efficient.

Common Pitfalls

  • Forgetting @Inheritance on the base class. Without it, Hibernate has no idea you have a hierarchy. It will treat each class as independent and you'll get separate, unlinked tables.
  • Mixing strategies across subclasses. If you want to override the strategy for a specific subclass, use @Inheritance on the subclass itself. Don't assume the base class's strategy always applies.
  • Wrong @GeneratedValue strategy. GenerationType.AUTO picks a strategy based on the dialect. On MySQL it may pick TABLE or SEQUENCE, which is slower than IDENTITY. Be explicit.
  • Assuming the parent is abstract. It doesn't have to be — you can save a plain Person row, as this tutorial does. If you want to force subclassing, mark the base class abstract.
  • Forgetting to register all classes. You must call addAnnotatedClass() for the base class and each subclass. Missing one means Hibernate won't map it.
  • Not closing the session. Even in a short test, close the session. In a real application, an unclosed session holds a JDBC connection from the pool.
  • Deprecated MySQL driver class. com.mysql.jdbc.Driver was removed in Connector/J 8. Use com.mysql.cj.jdbc.Driver.
  • Missing dialect. Hibernate can often guess the dialect from the connection URL, but on some setups it warns and defaults poorly. Set hibernate.dialect explicitly.
  • Expecting JOINED to be fast at scale. It's normalized and correct, but deep hierarchies plus large tables can make polymorphic queries slow. Benchmark before committing to it for very high-volume systems.

Wrapping Up

You now know how to map an inheritance hierarchy in Hibernate using the JOINED (table per subclass) strategy. It's the most normalized of the three, supports foreign keys back to the base type, and gives you clean NOT NULL constraints on subclass columns.

The key annotations to remember are @Inheritance(strategy = InheritanceType.JOINED) on the base class and @PrimaryKeyJoinColumn on each subclass to declare the shared primary key.

From here, you can compare strategies by switching the InheritanceType and observing the schema Hibernate generates. Try SINGLE_TABLE and TABLE_PER_CLASS on the same class structure — it's the fastest way to understand the trade-offs.

No comments: