Inheritance Mapping
Inheritance Mapping
Object-oriented design naturally reaches for inheritance to model real-world hierarchies: a Payment base type with concrete subclasses CreditCardPayment, BankTransferPayment, and CryptoPayment. Relational databases, however, have no native concept of subtyping. JPA bridges this mismatch with three distinct inheritance mapping strategies, each with measurable trade-offs in schema complexity, query performance, and nullable column discipline. Choosing the right one up front avoids painful schema migrations later.
The Three Strategies at a Glance
- SINGLE_TABLE — one table for the entire hierarchy, a discriminator column identifies the subtype.
- JOINED — one table per class; subtype tables share the same primary key and join back to the parent.
- TABLE_PER_CLASS — one standalone table per concrete class; no shared columns, no joins.
@Inheritance but omit strategy, JPA defaults to SINGLE_TABLE. Knowing this prevents accidental schema decisions.
Strategy 1 — SINGLE_TABLE
All classes in the hierarchy map to one table. The @DiscriminatorColumn annotation adds a column whose value tells Hibernate which subtype a given row represents. Subtype-specific columns that do not apply to a given row are stored as NULL.
The generated DDL for this hierarchy is a single payments table with columns for all three types plus the payment_type discriminator. A CreditCardPayment row has NULL in iban and bank_code; a BankTransferPayment row has NULL in masked_card_number and cardholder_name.
Payment entities requires exactly one SELECT with no joins. It is the best default when the hierarchy is shallow and the subtype-specific columns are few or nullable by nature. Enforce NOT NULL constraints at the application layer (Bean Validation @NotNull) rather than at the DB level when you choose this strategy.
Strategy 2 — JOINED
Each class in the hierarchy gets its own table. The parent table holds shared columns; each subtype table holds only the columns unique to that subtype and shares the same primary key value as the parent row. Hibernate generates a JOIN whenever it needs to materialize a full subtype object.
Loading a CreditCardPayment by ID generates:
A polymorphic query — SELECT p FROM Payment p — requires a LEFT OUTER JOIN across every subtype table. With three subtypes Hibernate generates three joins. This is correct but increasingly expensive as the hierarchy grows.
Discriminator Columns with JOINED
By default, JOINED does not require a discriminator column — Hibernate can infer the subtype from which join tables are present. However, adding one is recommended for readability and for tools that query the database directly:
Strategy 3 — TABLE_PER_CLASS
Each concrete class has a completely independent table that repeats the parent's columns. There is no shared parent table and no join between sibling tables.
Notice GenerationType.TABLE instead of IDENTITY. Because there is no shared parent table, database-level identity columns cannot guarantee uniqueness across all subtype tables. A JPA table sequence or UUID primary key is required.
A polymorphic query — SELECT p FROM Payment p — forces Hibernate to issue a UNION ALL across all concrete tables:
UNION ALL operations that cannot use indexes effectively on large tables. Use it only when you never need to query the hierarchy polymorphically — for example, when subtypes are always queried directly by their concrete type and the hierarchy is stable.
Choosing a Strategy — Decision Guide
- Shallow hierarchy, few subtypes, nullable fields acceptable → SINGLE_TABLE. Highest read performance, simplest schema.
- Deep hierarchy, strong DB integrity needed, NOT NULL constraints matter → JOINED. Normalized, flexible, pays join cost on polymorphic load.
- Subtypes are always queried independently, no polymorphic JPQL needed → TABLE_PER_CLASS. Avoid unless you are certain of the query pattern.
Polymorphic Queries and Repository Layer
Spring Data JPA works seamlessly with all three strategies. Declare a base repository against the parent type to query the whole hierarchy, and concrete repositories for subtype-specific finders:
Summary
JPA provides three inheritance mapping strategies to bridge OOP hierarchies to relational tables. SINGLE_TABLE stores everything in one table with a discriminator — fastest queries, but columns are nullable. JOINED normalizes each class into its own table — best integrity guarantees, pays a join per polymorphic load. TABLE_PER_CLASS repeats parent columns in every concrete table — avoid polymorphic queries, requires a non-IDENTITY key generator. Pick the strategy that matches your schema integrity requirements and your dominant query pattern, and resist changing it later — migration between strategies requires DDL changes on production data.