Lesson 9 / 25

Inheritance Mapping Strategies

Map class hierarchies with SINGLE_TABLE, JOINED, TABLE_PER_CLASS and @MappedSuperclass.

Tables for a class hierarchy

Relational databases have no inheritance, so JPA offers strategies. @MappedSuperclass is not an entity: it just shares mapped fields (such as id, createdAt, version) with subclasses, each of which has its own independent table; you cannot query the superclass polymorphically. SINGLE_TABLE (the default for @Inheritance) stores all subclasses in one table with a discriminator column (@DiscriminatorColumn); it is the fastest for polymorphic queries (no joins), but subclass-specific columns must be nullable, which weakens constraints. JOINED gives each class its own table, with subclass tables linked by primary key to the parent table; it keeps a normalised schema with proper NOT NULL constraints, at the cost of joins for every polymorphic query. TABLE_PER_CLASS gives each concrete class a complete table; polymorphic queries need UNIONs and sequence-based IDs, so it is rarely the best choice. Choose by query patterns: many polymorphic queries favour SINGLE_TABLE; strict constraints and subclass-heavy queries favour JOINED. Often, composition or a simple type field is better than mapping deep hierarchies.

SINGLE_TABLE and JOINED side by side

Payments in one table with a discriminator; accounts split across joined tables.

@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) protected Long id;
    @Column(nullable = false, updatable = false) protected Instant createdAt = Instant.now();
}

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "method", length = 10)
public abstract class Payment extends BaseEntity {
    protected BigDecimal amount;
}
@Entity @DiscriminatorValue("UPI")  public class UpiPayment  extends Payment { private String vpa; }
@Entity @DiscriminatorValue("CARD") public class CardPayment extends Payment { private String last4; }
// one table: payments(id, created_at, method, amount, vpa NULL, last4 NULL)

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Account extends BaseEntity { protected String owner; }
@Entity public class SavingsAccount extends Account { @Column(nullable = false) private BigDecimal rate; }
@Entity public class LoanAccount    extends Account { @Column(nullable = false) private BigDecimal principal; }
// tables: account(id, created_at, owner), savings_account(id FK, rate), loan_account(id FK, principal)

Polymorphic queries decide the strategy

If you mostly query select p from Payment p, SINGLE_TABLE avoids joins. If you mostly query one subtype and need NOT NULL constraints on its columns, JOINED is cleaner. Measure the queries you actually run.

Quick check: Which inheritance strategy stores all subclasses in one table with a discriminator column?

  • JOINED
  • TABLE_PER_CLASS
  • SINGLE_TABLE
  • @MappedSuperclass
Answer

SINGLE_TABLE — SINGLE_TABLE uses one table plus a discriminator to tell rows apart.