Hibernate supports mapping Java class hierarchies to relational database tables. There are four strategies:
| Strategy | Table Structure | Pros | Cons |
|---|---|---|---|
| SINGLE_TABLE | One table for all classes | Best performance, simple queries | Many nullable columns, no NOT NULL constraints on subclass fields |
| TABLE_PER_CLASS | One table per concrete class | No joins needed for single type | Polymorphic queries use UNION (slow), duplicate columns |
| JOINED | One table per class (parent + subclass tables) | Normalized, no nulls | Joins required for every query |
| @MappedSuperclass | No table for superclass | Share fields without polymorphism | Cannot query the superclass type |
import jakarta.persistence.*;
// SINGLE_TABLE: all subclasses stored in one table
// A discriminator column identifies the type
@Entity
@Table(name = "payments")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type",
discriminatorType = DiscriminatorType.STRING)
public abstract class Payment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double amount;
private java.time.LocalDate paymentDate;
// getters/setters...
}
@Entity
@DiscriminatorValue("CREDIT_CARD")
public class CreditCardPayment extends Payment {
private String cardNumber;
private String cardHolder;
// Stored in 'payments' table with payment_type = 'CREDIT_CARD'
// cardNumber and cardHolder are nullable for other types
}
@Entity
@DiscriminatorValue("BANK_TRANSFER")
public class BankTransferPayment extends Payment {
private String bankAccount;
private String bankCode;
// Stored in 'payments' table with payment_type = 'BANK_TRANSFER'
}
// Polymorphic query - fetches all payment types
// SELECT * FROM payments (single table, no joins)
// List<Payment> all = em.createQuery("FROM Payment", Payment.class).getResultList();
// TABLE_PER_CLASS: each concrete class has its own complete table
// No discriminator column needed
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Vehicle {
@Id @GeneratedValue(strategy = GenerationType.AUTO) // TABLE_PER_CLASS requires AUTO
private Long id;
private String manufacturer;
private int year;
}
@Entity
@Table(name = "cars")
public class Car extends Vehicle {
private int numDoors;
private String fuelType;
// table 'cars' has: id, manufacturer, year, num_doors, fuel_type
}
@Entity
@Table(name = "motorcycles")
public class Motorcycle extends Vehicle {
private String type; // sport, cruiser, touring
// table 'motorcycles' has: id, manufacturer, year, type
}
// Polymorphic query uses UNION ALL (can be slow):
// SELECT id, manufacturer, year, num_doors, null AS type FROM cars
// UNION ALL
// SELECT id, manufacturer, year, null AS num_doors, type FROM motorcycles
// JOINED: normalized - parent table + subclass tables joined by FK
// Best for data integrity; requires JOIN for every query
@Entity
@Table(name = "employees")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Employee {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private double baseSalary;
}
@Entity
@Table(name = "full_time_employees")
@PrimaryKeyJoinColumn(name = "employee_id") // FK to employees.id
public class FullTimeEmployee extends Employee {
private double bonus;
private int vacationDays;
// Query: SELECT e.*, f.bonus, f.vacation_days
// FROM employees e JOIN full_time_employees f ON e.id = f.employee_id
}
@Entity
@Table(name = "part_time_employees")
@PrimaryKeyJoinColumn(name = "employee_id")
public class PartTimeEmployee extends Employee {
private double hourlyRate;
private int hoursPerWeek;
}
// @MappedSuperclass: NOT an entity - no table created for it
// Subclasses inherit fields but cannot be queried polymorphically
import jakarta.persistence.*;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// getters/setters...
}
// Each subclass gets its own table with the inherited fields
@Entity
@Table(name = "products")
public class Product extends BaseEntity {
private String name;
private double price;
// table 'products': id, created_at, updated_at, name, price
}
@Entity
@Table(name = "categories")
public class Category extends BaseEntity {
private String title;
// table 'categories': id, created_at, updated_at, title
}
// NOTE: Cannot query BaseEntity directly:
// em.createQuery("FROM BaseEntity") // ERROR - not an entity!
@Repository
@Transactional(readOnly = true)
public class PaymentRepository {
@PersistenceContext
private EntityManager em;
// Polymorphic query - returns all Payment subtypes
public List<Payment> getAllPayments() {
return em.createQuery("FROM Payment p ORDER BY p.paymentDate DESC",
Payment.class)
.getResultList();
}
// Query only a specific subtype
public List<CreditCardPayment> getCreditCardPayments() {
return em.createQuery(
"FROM CreditCardPayment p WHERE p.amount > :min",
CreditCardPayment.class)
.setParameter("min", 100.0)
.getResultList();
}
// Use TYPE() function to filter by subtype in polymorphic query
public List<Payment> getCreditCardAndBankPayments() {
return em.createQuery(
"FROM Payment p WHERE TYPE(p) IN (CreditCardPayment, BankTransferPayment)",
Payment.class)
.getResultList();
}
// instanceof check in Java after fetching
public void processPayments() {
List<Payment> payments = getAllPayments();
for (Payment p : payments) {
if (p instanceof CreditCardPayment cc) {
System.out.println("Card: " + cc.getCardNumber());
} else if (p instanceof BankTransferPayment bt) {
System.out.println("Bank: " + bt.getBankAccount());
}
}
}
}
SINGLE_TABLE keeps polymorphic reads simple but creates nullable subtype columns and a wide table. JOINED normalizes subtype data but adds joins to polymorphic queries. TABLE_PER_CLASS duplicates common columns and may require unions. @MappedSuperclass shares mapping declarations without creating a polymorphic entity root.
Choose from actual query patterns, constraints, migration needs, and subtype growth. Avoid inheritance when composition models the domain more clearly.
A discriminator is persisted data and should use stable values rather than Java class names that may change during refactoring. Add database checks where practical so an unknown discriminator cannot create an unreadable row.
Changing inheritance strategy is a data migration, not an annotation-only refactor. Create the new shape, copy and validate rows, support a controlled transition, and compare polymorphic query results before retiring the old tables.
Every subtype shares one table, so columns used by only one subtype must normally be nullable for rows of other subtypes. The strategy gives simple polymorphic queries and avoids joins, but a wide hierarchy can create sparse tables and weak subtype-specific constraints.
JOINED stores common fields in a base table and subtype fields in separate tables. Loading a concrete subtype or querying polymorphically requires joins, sometimes across several tables.
Use @MappedSuperclass to share mapped fields such as id, timestamps, or audit data when the base type itself should not be queried or stored as an entity. Its fields are copied into subclass mappings, but it has no table identity or polymorphic queries.
Explore 500+ free tutorials across 20+ languages and frameworks.