Hibernate Table Per Concrete Class (TABLE_PER_CLASS) Tutorial (2026)
Last Updated: | Author: Deep Singh
Hibernate gives you three ways to map an inheritance hierarchy to database tables: SINGLE_TABLE, JOINED, and TABLE_PER_CLASS. We've already looked at JOINED (table per subclass). In this tutorial, we're focusing on the third one — TABLE_PER_CLASS, also called table per concrete class.
It's the strategy with the biggest trade-offs. There's no shared parent table — each concrete class gets its own table with a full copy of the inherited fields. It's less normalized but avoids JOINs on single-class queries. Let's see how it works.
How TABLE_PER_CLASS Differs from the Others
Here's a quick comparison to put TABLE_PER_CLASS in context:
- SINGLE_TABLE — one shared table with a discriminator column. Fast reads, but no NOT NULL on subclass columns.
- JOINED (table per subclass) — a base table plus a table per subclass, sharing the primary key. Normalized, supports NOT NULL, uses JOINs for reads.
- TABLE_PER_CLASS (table per concrete class) — a fully independent table per concrete class. Shared fields are duplicated. Polymorphic queries use UNION. This is the one we're covering here.
When you use TABLE_PER_CLASS, Hibernate does not create a table for the abstract or non-concrete parent class. Only the concrete classes get tables.
Important: If you save a plain Person object (like we did in the JOINED tutorial), Hibernate must create a table for it. That means Person has to be concrete, not abstract. In practice, this strategy is usually used with an abstract parent class so you don't accidentally persist 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 from 2010. The MySQL connector has since moved to com.mysql:mysql-connector-j. The driver class 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.TABLE_PER_CLASS)
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
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;
}
}
Critical change — GenerationType.TABLE:
This is the single most important difference from the JOINED tutorial. With TABLE_PER_CLASS, you cannot use GenerationType.IDENTITY or GenerationType.SEQUENCE because the IDs of the different concrete tables would collide. Two different tables can't both start their AUTO_INCREMENT at 1 and remain globally unique across the hierarchy.
Hibernate solves this by using GenerationType.TABLE — a dedicated hibernate_sequences table stores the next available ID, and every concrete table draws from that single pool. It's slower than IDENTITY because it takes a row lock on the sequence table, but it guarantees unique IDs across all tables in the hierarchy.
What about the original's GenerationType.AUTO? AUTO lets Hibernate pick. On MySQL, it may pick IDENTITY — which then breaks with TABLE_PER_CLASS at runtime with a "Cannot use identity column key generation with TABLE avoids that failure.
Employee.java — Subclass
package com.deepsingh44.model;
import javax.persistence.Entity;
@Entity
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;
@Entity
public class Student extends Person {
private float marks;
public float getMarks() {
return marks;
}
public void setMarks(float marks) {
this.marks = marks;
}
}
Note on the subclasses: They don't need any special annotations — no @PrimaryKeyJoinColumn like in the JOINED strategy. That's because each subclass table is entirely independent.
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: Same as the JOINED tutorial — the transaction is now opened before the saves (the original called session.beginTransaction() after the saves), errors are caught with rollback, and the session and factory are properly closed.
Note: Saving a plain Person works here because Person is a concrete class. If you want to prevent that, mark it abstract — then the only persisted entities would be Employee and Student.
How the Tables Are Created
With hbm2ddl.auto=update, Hibernate creates four tables when the app starts:
CREATE TABLE person (
id INT NOT NULL,
name VARCHAR(255),
PRIMARY KEY (id)
);
CREATE TABLE employee (
id INT NOT NULL,
name VARCHAR(255),
salary FLOAT,
PRIMARY KEY (id)
);
CREATE TABLE student (
id INT NOT NULL,
name VARCHAR(255),
marks FLOAT,
PRIMARY KEY (id)
);
CREATE TABLE hibernate_sequences (
sequence_name VARCHAR(255) NOT NULL,
next_val BIGINT,
PRIMARY KEY (sequence_name)
);
Notice the differences from JOINED:
- No foreign keys. Each table is completely standalone. There's no relationship between
person,employee, andstudent. - Duplicated columns. The
namefield appears in all three tables. If you add a new field toPerson, you'll need to alter every concrete table. - The sequences table. That's the shared ID pool that lets the different tables have globally unique primary keys.
Persongets its own table. BecausePersonis concrete, Hibernate creates a table for it too. If it were abstract, that table wouldn't exist.
What Hibernate Does When You Save
When you call session.save(emp):
- Hibernate asks the
hibernate_sequencestable for the next ID. - It inserts a row into the
employeetable with that ID, the name, and the salary.
No insert into person happens — the employee table has its own copy of the name field.
What a Polymorphic Query Looks Like
Here's where TABLE_PER_CLASS starts to hurt. When you query for all Person instances (which includes employees and students), Hibernate must UNION the three tables:
SELECT id, name FROM person
UNION ALL
SELECT id, name, salary FROM employee
UNION ALL
SELECT id, name, marks FROM student;
That UNION grows with every subclass you add. On a hierarchy with five subclasses and millions of rows, this query becomes very expensive. If you find yourself running polymorphic queries regularly, consider JOINED instead.
What a Single-Class Query Looks Like
The good news: querying a specific concrete class is fast. Loading Employee by ID hits only the employee table:
SELECT id, name, salary FROM employee WHERE id = ?;
No JOIN needed. This is the main advantage of TABLE_PER_CLASS — single-class operations are as fast as they can possibly be.
Common Pitfalls
- Using IDENTITY or SEQUENCE generation. This is the biggest trap. With TABLE_PER_CLASS, you must use
GenerationType.TABLE. If you useAUTOorIDENTITY, Hibernate will either throw at startup or produce colliding IDs at runtime. - Making the base class concrete. If you don't want a table for
Person, mark itabstract. Making it concrete creates an extra table and lets users accidentally persist a barePerson. - Forgetting to register all classes. You must call
addAnnotatedClass()for the base class and every subclass. Missing one means Hibernate won't map it. - Adding a field to the base class later. Because fields are duplicated, adding a field to
Personmeans alteringemployee,student, and any other concrete table. This gets painful in production. - Assuming polymorphic queries are cheap. They aren't. Every polymorphic query is a UNION over all concrete tables. Consider JOINED if this is a common pattern.
- Mixing strategies. Don't apply TABLE_PER_CLASS to one subclass and JOINED to another in the same hierarchy — Hibernate won't know what to do.
- Broken transaction order. Open the transaction before saving. Relying on Hibernate to auto-begin is fragile and can hide bugs.
- Deprecated MySQL driver class.
com.mysql.jdbc.Driverwas removed in Connector/J 8. Usecom.mysql.cj.jdbc.Driver. - No dialect set. Hibernate can usually guess, but on some setups it defaults poorly. Set
hibernate.dialectexplicitly.
Wrapping Up
You now know how to map an inheritance hierarchy in Hibernate using InheritanceType.TABLE_PER_CLASS. It's the least normalized of the three strategies, but it has its place: single-class queries are extremely fast, and there are no joins to worry about when loading a specific subclass.
The key trade-offs to remember:
- Use TABLE_PER_CLASS when you rarely query polymorphically, you want the fastest possible single-class reads, and you don't mind duplicated columns.
- Use JOINED when you frequently query polymorphically, you want a normalized schema, and you can afford the JOIN cost.
- Use SINGLE_TABLE when your hierarchy is shallow and simple, and read performance matters most.
The single biggest gotcha with TABLE_PER_CLASS is the ID generation strategy — it must be GenerationType.TABLE, or the whole thing breaks. Mark that down.
No comments:
Post a Comment