💾 Section 16 · Question #30

What Inheritance Types Does JPA Support

The object-oriented paradigm in Java relies on polymorphism and class inheritance, while the relational database model operates on flat tables and foreign keys (the Object-Relat...

The object-oriented paradigm in Java relies on polymorphism and class inheritance, while the relational database model operates on flat tables and foreign keys (the Object-Relational Impedance Mismatch problem). The JPA specification defines three core inheritance mapping strategies (SINGLE_TABLE, JOINED, TABLE_PER_CLASS) using the @Inheritance annotation, alongside the non-polymorphic property-sharing pattern @MappedSuperclass.


🟢 Junior Level

The 3 JPA Inheritance Strategies

The inheritance strategy is declared using @Inheritance(strategy = InheritanceType...) on the root entity of the hierarchy:

               3 INHERITANCE MAPPING STRATEGIES IN RELATIONAL DATABASES
               
   1. SINGLE_TABLE (Default)          2. JOINED                     3. TABLE_PER_CLASS
      ┌─────────────────────────┐        ┌──────────────────────┐      ┌──────────────────────┐
      │        payments         │        │    payments (Base)   │      │    card_payments     │
      ├─────────────────────────┤        ├──────────────────────┤      ├──────────────────────┤
      │ id, dtype, amount,      │        │ id, amount           │      │ id, amount, card_num │
      │ card_num, cash_date     │        └──────────┬───────────┘      └──────────────────────┘
      └─────────────────────────┘                   │ PK/FK                    ┌──────────────────────┐
      (All classes in 1 table)           ┌──────────┴───────────┐      │    cash_payments     │
                                         │    card_payments     │      ├──────────────────────┤
                                         ├──────────────────────┤      │ id, amount, cash_date│
                                         │ id (FK), card_num    │      └──────────────────────┘
                                         └──────────────────────┘      (Isolated standalone tables)

Basic Configuration Example:

// Root base entity:
@Entity
@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 BigDecimal amount;
}

// Subclass 1: Credit card payment
@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    private String cardNumber;
}

// Subclass 2: Cash payment
@Entity
@DiscriminatorValue("CASH")
public class CashPayment extends Payment {
    private String cashierName;
}

🟡 Middle Level

Detailed Strategy Analysis & Trade-offs

1. InheritanceType.SINGLE_TABLE (Single Table for Entire Hierarchy)

All fields of all classes across the hierarchy are stored in a single database table. A discriminator column (DTYPE by default) identifies the concrete subclass.

  • Pros:
    • Maximum read and write performance (zero SQL JOIN or UNION operations).
    • Polymorphic queries execute directly: SELECT * FROM payments.
  • Cons:
    • Table sparsity (many NULL values for subclass-specific columns).
    • Cannot enforce database-level NOT NULL constraints on subclass fields (the card_number column must allow NULL to permit inserting CashPayment rows).

2. InheritanceType.JOINED (Normalized Table per Class)

The root class maps to a base table containing common columns, while each subclass maps to its own separate table containing specific columns, joined via primary/foreign key pairs.

  • Pros:
    • Fully normalized database schema (Third Normal Form - 3NF).
    • Subclass columns fully support NOT NULL database integrity constraints.
  • Cons:
    • Reading polymorphic graphs requires executing an SQL LEFT OUTER JOIN across all subclass tables.
    • Inserting a subclass record requires two sequential SQL INSERT statements (first into base, then into child table).

3. InheritanceType.TABLE_PER_CLASS (Table per Concrete Class)

Each concrete subclass maps to a standalone database table containing both its own fields and duplicated columns inherited from the base class.

  • Pros: Each table is self-contained without sparse NULLs or joins when querying a known concrete subclass.
  • Cons: Polymorphic queries targeting the base class (SELECT p FROM Payment p) generate heavy UNION ALL queries that bypass index optimization.

Architectural Comparison Matrix

Architectural Criterion SINGLE_TABLE JOINED TABLE_PER_CLASS
Schema Normalization ❌ Low (Sparse NULLs) ✅ High (3NF) ⚠️ Medium (Column duplication)
NOT NULL Constraints ❌ Impossible for subclass fields ✅ Fully supported ✅ Fully supported
INSERT Statement Count ⚡ 1 INSERT 🐢 2+ INSERTs ⚡ 1 INSERT
Polymorphic Query Cost ⚡ 1 Query (No JOIN) 🐢 Heavy LEFT OUTER JOIN 🚨 Slow UNION ALL scan
Discriminator Column Required (@DiscriminatorColumn) Optional Not used
Recommendation Default choice for 2–5 subclasses Normalized enterprise systems Rare (Avoid when polymorphism needed)

🔴 Senior Level

@MappedSuperclass vs. Polymorphic Inheritance

A frequent conceptual confusion in technical interviews is treating @MappedSuperclass as an inheritance strategy:

// Base class for shared audit fields:
@MappedSuperclass
public abstract class BaseAuditableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreationTimestamp
    private Instant createdAt;
}

@Entity
public class User extends BaseAuditableEntity { ... }

@Entity
public class Order extends BaseAuditableEntity { ... }

Fundamental Invariants of @MappedSuperclass:

  1. A @MappedSuperclass is NOT a JPA entity (@Entity is absent).
  2. It has no dedicated database table.
  3. Does not support polymorphic queries: SELECT b FROM BaseAuditableEntity b fails compilation because the class is not an entity known to Hibernate.
  4. Other entities cannot define associations to it (e.g., @ManyToOne BaseAuditableEntity entity is illegal).

The Polymorphic Foreign Key Deadlock with TABLE_PER_CLASS

Consider an entity Order referencing an abstract payment:

@Entity
public class Order {

    @Id
    private Long id;

    @OneToOne
    @JoinColumn(name = "payment_id")
    private Payment payment; // Payment uses TABLE_PER_CLASS
}

Why This Creates an Architectural Deadlock:

  • The column orders.payment_id must enforce referential integrity.
  • However, the payments table does not exist; only isolated card_payments and cash_payments tables exist.
  • Relational database engines cannot declare a single Foreign Key constraint referencing multiple tables simultaneously!
  • To load the associated payment, Hibernate must issue separate queries or complex outer unions against every possible subclass table.
  • Senior Rule: Never map @ManyToOne or @OneToOne associations to an abstract base class configured with TABLE_PER_CLASS.

4 Tricky Questions

1. In SINGLE_TABLE, how do you enforce that a subclass field (e.g., cardNumber) is mandatory if the database column must remain NULLable?

Answer:

  1. At the database DDL level, the column must permit NULL values to accommodate other subclasses.
  2. Strategy 1 (Database Check Constraint): Add a conditional table-level check constraint:
    ALTER TABLE payments ADD CONSTRAINT chk_card_number 
    CHECK (payment_type != 'CARD' OR card_number IS NOT NULL);
    
  3. Strategy 2 (Bean Validation): Place the @NotNull annotation on the cardNumber property inside the CardPayment subclass. Hibernate Validator intercepts empty values during the PrePersist phase before SQL execution.

2. What happens when executing a paginated polymorphic query SELECT p FROM Payment p under TABLE_PER_CLASS?

Answer:

  1. Hibernate generates an SQL query wrapping all subclass tables in a UNION ALL:
    SELECT p.id, p.amount, p.card_num, NULL AS cash_date, 1 AS clazz_ FROM card_payments p
    UNION ALL
    SELECT p.id, p.amount, NULL AS card_num, p.cash_date, 2 AS clazz_ FROM cash_payments p
    LIMIT 10 OFFSET 20;
    
  2. The database engine must execute full scans of both tables, materialize them into an intermediate in-memory temporary table, and only then apply sorting and LIMIT/OFFSET.
  3. Query performance degrades severely as table sizes grow.

3. How do you query a specific subclass and perform downcasting in JPQL under JOINED or SINGLE_TABLE?

Answer:

  1. Use the TYPE(e) expression in JPQL to filter by concrete subclass:
    List<Payment> cards = entityManager.createQuery(
        "SELECT p FROM Payment p WHERE TYPE(p) = CardPayment", 
        Payment.class
    ).getResultList();
    
  2. Use the TREAT operator for downcasting subclass properties in the WHERE or SELECT clauses:
    List<Payment> visa = entityManager.createQuery(
        "SELECT p FROM Payment p WHERE TREAT(p AS CardPayment).cardNumber LIKE '4%'", 
        Payment.class
    ).getResultList();
    

4. If an entity is loaded via a lazy association order.getPayment(), will order.getPayment() instanceof CardPayment evaluate to true?

Answer:

  1. No, it will evaluate to false!
  2. When the association is lazy, Hibernate generates a ByteBuddy proxy based on the declared base class: Payment$ByteBuddy$....
  3. Because the proxy is a direct subclass of Payment and does not inherit from CardPayment, Java’s native instanceof CardPayment check returns false, even if the underlying database row represents a card payment!
  4. Solution: Unproxy the entity before checking:
    Payment realPayment = Hibernate.unproxy(order.getPayment(), Payment.class);
    boolean isCard = realPayment instanceof CardPayment;
    // Or using Hibernate utility:
    boolean isCardType = Hibernate.getClass(order.getPayment()) == CardPayment.class;
    

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“JPA provides 3 inheritance strategies: SINGLE_TABLE (all classes in one table with a discriminator column; highest performance, but requires nullable subclass columns), JOINED (normalized base table plus subclass tables; supports NOT NULL constraints, but requires joins), and TABLE_PER_CLASS (independent tables with duplicated fields; generates expensive UNION ALL queries). @MappedSuperclass is not an inheritance strategy, but a code reuse pattern for shared audit properties without an entity table or polymorphic querying. In roughly 80% of enterprise systems, SINGLE_TABLE remains the preferred standard due to superior query performance.”


Key Production Metrics & Indicators

  • Execution Time of Polymorphic Root Queries: Monitored to detect degradation in JOINED and TABLE_PER_CLASS hierarchies.
  • Table Column Count in SINGLE_TABLE: Hierarchies exceeding 40–50 columns indicate schema bloat, signaling a need to transition to JOINED or decompose into independent aggregates.

Red Flags (What NOT to Say)

  • ❌ “@MappedSuperclass is a fourth JPA inheritance strategy.” — It is not an inheritance strategy; it creates no entity or table and does not support polymorphic queries.
  • ❌ “SINGLE_TABLE supports database-level NOT NULL constraints on subclass-specific columns.” — False; columns must be nullable at the DB level for other subclasses to be inserted.
  • ❌ “TABLE_PER_CLASS is ideal for @ManyToOne polymorphic associations.” — Relational databases cannot create a single foreign key pointing to multiple distinct tables.
  • ❌ “Java’s instanceof operator works reliably on lazy proxy associations.” — False; lazy ByteBuddy proxies of the base class fail instanceof SubClass checks until unproxied.