Tutorials Logic, IN info@tutorialslogic.com

Hibernate Inheritance Mapping SINGLE_TABLE, JOINED

Inheritance Strategies Overview

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

SINGLE_TABLE and TABLE_PER_CLASS Strategies

SINGLE_TABLE and TABLE_PER_CLASS Strategies
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();

Inheritance Strategies Overview - Java Example

Inheritance Strategies Overview - Java Example
// 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 Strategy and @MappedSuperclass

JOINED Strategy and @MappedSuperclass - Java Example

JOINED Strategy and @MappedSuperclass - Java Example
// 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;
}

Polymorphic Queries

Polymorphic Queries
// @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!

JOINED Strategy and @MappedSuperclass - Java Example 2

JOINED Strategy and @MappedSuperclass - Java Example 2
@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());
            }
        }
    }
}

Choose a Strategy from Query and Schema Costs

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.

Plan Discriminators and Migrations

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.

Before you move on

Hibernate Inheritance Mapping SINGLE_TABLE, JOINED Mastery Check

5 checks
  • SINGLE_TABLE is the one table for all classes operator; Best performance, simple queries produces Many nullable columns, no NOT NULL constraints on subclass fields.
  • TABLE_PER_CLASS is the one table per concrete class operator; No joins needed for single type produces Polymorphic queries use UNION (slow), duplicate columns.
  • JOINED is the one table per class (parent + subclass tables) operator; Normalized, no nulls produces Joins required for every query.
  • @MappedSuperclass is the no table for superclass operator; Share fields without polymorphism produces Cannot query the superclass type.
  • Inheritance Strategies Overview includes One table for all classes, One table per concrete class, One table per class (parent + subclass tables), and No table for superclass.

Hibernate Questions Learners Ask

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.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.