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
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 |
|
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.