Inheritance Mapping

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

Java’s class hierarchies have no direct SQL counterpart — one of the paradigm-mismatch friction points named in Architecture. Hibernate offers three strategies for flattening a hierarchy onto tables, plus @MappedSuperclass for sharing mapped state without the hierarchy itself being polymorphic in queries.

SINGLE_TABLE

The whole hierarchy lives in one table; a discriminator column says which subtype each row represents.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal amount;
}

@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    private String last4Digits;
}

@Entity
@DiscriminatorValue("BANK_TRANSFER")
public class BankTransferPayment extends Payment {
    private String iban;
}

Fastest strategy — every query is against one table, no joins ever needed for polymorphic access. The cost: every subclass-specific column (last4Digits, iban) must be nullable, since any given row uses only the columns for its own subtype, and the table grows a column per subtype added over time. @DiscriminatorFormula replaces the plain column with a SQL expression when the discriminator has to be derived (e.g. from an existing legacy column) rather than stored directly.

JOINED

One table per class in the hierarchy, each holding only that class’s own columns, joined to the parent table on the shared primary key.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal amount;
}

@Entity
public class CardPayment extends Payment {
    private String last4Digits; // in its own "card_payment" table, PK = FK to payment.id
}

Fully normalized (no nullable-by-construction columns, no wasted space), but a CardPayment row now requires a join across payment and card_payment, and a polymorphic query across the whole hierarchy joins in every subtype’s table.

TABLE_PER_CLASS

One table per concrete class, each repeating every inherited column:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private BigDecimal amount; // repeated in every concrete subtype's table
}

No joins for accessing one concrete subtype directly, but a polymorphic query across the hierarchy needs a UNION ALL across every concrete table — generally the least-used of the three strategies, since it combines denormalization (repeated columns) with the worst polymorphic-query cost. IDENTITY generation cannot be used with this strategy (each subtype’s own auto-increment sequence would collide across tables); use SEQUENCE or TABLE instead.

@MappedSuperclass

For sharing mapped attributes (an id-generation pattern, audit columns) across otherwise-unrelated entities without the hierarchy being queryable polymorphically — there is no common entity table or query root, just inherited mapping metadata:

@MappedSuperclass
public abstract class AuditableEntity {
    @Column(updatable = false)
    private Instant createdAt;
    private Instant updatedAt;
}

@Entity
public class Book extends AuditableEntity { /* ... */ }

@Entity
public class Author extends AuditableEntity { /* ... */ }

Book and Author each get their own independent created_at/updated_at columns; there is no FROM AuditableEntity query and no common table. Compare with Spring Data JPA's auditing support, which is commonly layered on top of exactly this pattern via AbstractAuditable/@EntityListeners(AuditingEntityListener.class).

Choosing a strategy, and polymorphic query cost

SINGLE_TABLE JOINED TABLE_PER_CLASS

Storage

One table, nullable subtype columns

Normalized, no nullable columns

Denormalized, repeated columns

Polymorphic query cost

Cheapest — no join

One join per level queried across

UNION ALL across concrete tables

Concrete-subtype-only query cost

Cheapest — no join

One join to the parent table

Cheapest — no join

Typical fit

Small hierarchies, frequent polymorphic queries

Larger hierarchies, storage/normalization matters

Rare; concrete-type access dominates, hierarchy is shallow

SINGLE_TABLE is Hibernate’s default and the right starting point for most hierarchies; reach for JOINED when subtype-specific columns are numerous or storage waste from `SINGLE_TABLE’s nullable columns becomes a real concern.

Table shapes compared

SINGLE_TABLE