JPA Inheritance Mapping and Lifecycle Callbacks
🎧 Focus sounds
Soft beat sounds that many learners like while reading. Pick one, press Play, and keep studying.
Keep the volume low. These sounds are for comfort while studying, not a medical treatment. If you have epilepsy or are sensitive to pulsing sounds, use the background noise only.
In this lesson
Your Java model uses inheritance (a Payment can be a UpiPayment or CardPayment), but relational tables don’t have inheritance. JPA offers several strategies to bridge the gap, plus callbacks to run code automatically when entities are saved or updated.
@MappedSuperclass: share fields, not a table#
Most entities need id, createdAt and updatedAt. Put them in a base class:
package com.vitalstack;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@MappedSuperclass // not an entity: its fields are copied into each subclass table
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
protected LocalDateTime createdAt;
protected LocalDateTime updatedAt;
@PrePersist // runs just before INSERT
void onCreate() { createdAt = updatedAt = LocalDateTime.now(); }
@PreUpdate // runs just before UPDATE
void onUpdate() { updatedAt = LocalDateTime.now(); }
public Long getId() { return id; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
You can’t query a @MappedSuperclass (select b from BaseEntity b fails). It’s just a code-sharing tool.
Entity inheritance strategies#
When you do want polymorphic queries (select p from Payment p returning UPI and card payments), make the parent an entity and choose a strategy:
1. SINGLE_TABLE (default): one table for the whole hierarchy#
package com.vitalstack;
import jakarta.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "method") // which subclass is this row?
public abstract class Payment extends BaseEntity {
protected int amount;
public int getAmount() { return amount; }
public abstract String describe();
}
package com.vitalstack;
import jakarta.persistence.*;
@Entity
@DiscriminatorValue("UPI")
public class UpiPayment extends Payment {
private String vpa;
protected UpiPayment() { }
public UpiPayment(int amount, String vpa) { this.amount = amount; this.vpa = vpa; }
public String describe() { return "UPI ₹" + amount + " from " + vpa; }
}
package com.vitalstack;
import jakarta.persistence.*;
@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
private String last4;
protected CardPayment() { }
public CardPayment(int amount, String last4) { this.amount = amount; this.last4 = last4; }
public String describe() { return "Card ₹" + amount + " ****" + last4; }
}
package com.vitalstack;
import jakarta.persistence.*;
public class Main {
public static void main(String[] args) throws Exception {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("school");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(new UpiPayment(499, "asha@okaxis"));
CardPayment card = new CardPayment(1299, "4242");
em.persist(card);
em.getTransaction().commit();
Thread.sleep(20);
em.getTransaction().begin();
card.amount = 999; // triggers @PreUpdate at flush
em.getTransaction().commit();
System.out.println("updated later than created? " + card.getUpdatedAt().isAfter(card.getCreatedAt()));
em.clear();
em.createQuery("select p from Payment p order by p.id", Payment.class) // polymorphic query
.getResultList()
.forEach(p -> System.out.println(p.describe()));
em.close();
emf.close();
}
}
Generated schema and output:
create table Payment (amount integer not null, createdAt timestamp(6), id bigint generated by default as identity,
updatedAt timestamp(6), method varchar(31) not null, last4 varchar(255), vpa varchar(255), primary key (id))
updated later than created? true
UPI ₹499 from asha@okaxis
Card ₹999 ****4242
One table, a method discriminator column, and nullable columns for subclass fields. Fastest for queries (no joins), but subclass columns can’t be NOT NULL.
2. JOINED: one table per class, linked by primary key#
@Inheritance(strategy = InheritanceType.JOINED)
payment(id, amount…), upi_payment(id, vpa), card_payment(id, last4). Clean, normalised, subclass columns can be NOT NULL, but every query needs joins.
3. TABLE_PER_CLASS: one complete table per concrete class#
Each subclass table repeats all parent columns. Polymorphic queries need UNIONs: slow. Rarely used.
| Strategy | Tables | Polymorphic queries | Constraints | Use when |
|---|---|---|---|---|
| SINGLE_TABLE | 1 | fastest | subclass fields nullable | few subclass fields (most cases) |
| JOINED | 1 + one per subclass | joins | full | many subclass-specific fields, data integrity matters |
| TABLE_PER_CLASS | one per concrete class | unions | full | almost never |
| @MappedSuperclass | one per subclass | not possible | full | just sharing fields |
Before reaching for entity inheritance, ask whether a single entity with an enum type field (or an @Embeddable) would do. Inheritance in the database is harder to change later than in Java.
Lifecycle callbacks#
| Annotation | Runs |
|---|---|
@PrePersist / @PostPersist | before / after INSERT |
@PreUpdate / @PostUpdate | before / after UPDATE |
@PreRemove / @PostRemove | before / after DELETE |
@PostLoad | after loading from the database |
Typical uses: timestamps, normalising data (email = email.toLowerCase()), computing derived fields.
Entity listeners: reusable callbacks#
public class AuditListener {
@PrePersist
public void beforeSave(Object entity) {
System.out.println("Saving " + entity.getClass().getSimpleName());
}
}
@Entity
@EntityListeners(AuditListener.class)
public class Course { ... }
Spring Data JPA offers ready-made auditing (@CreatedDate, @LastModifiedBy) built on this mechanism.
Callback rules#
- Don’t call the EntityManager or run queries inside callbacks (undefined behaviour).
- Keep them fast and side-effect free (no emails, no HTTP calls). Use events or a service layer for that.
@PreUpdateruns only if Hibernate detects a change.
Exercise#
- Create a
BaseEntitywith audit fields and make two entities extend it. Check both tables have the columns. - Switch the Payment hierarchy to
JOINEDand compare the generated tables and the SQL of the polymorphic query. - Add a
@PrePersistthat lowercases and trims emails on aStudententity. - Add a
NetBankingPaymentsubclass and query only card payments withselect p from CardPayment p.
Hint for 2
You’ll see three tables and a query that left-joins the subclass tables, with a case expression to work out each row’s type.
Interview corner#
- Inheritance strategies in JPA? SINGLE_TABLE, JOINED, TABLE_PER_CLASS (plus @MappedSuperclass for non-entity base classes).
- @MappedSuperclass vs @Entity parent? MappedSuperclass only shares mappings (no table, not queryable); an entity parent allows polymorphic queries and associations.
- What is a discriminator column? A column that stores which subclass a row belongs to (SINGLE_TABLE, optionally JOINED).
- What are lifecycle callbacks? Methods annotated @PrePersist, @PostLoad etc. that JPA calls around entity operations.
Summary#
@MappedSuperclassshares common fields like id and audit timestamps.- SINGLE_TABLE is fast and simple; JOINED is normalised; TABLE_PER_CLASS is rarely worth it.
- Callbacks and entity listeners run code automatically around persistence events.
Next: transactions and locking for safe concurrent updates.
Quick quiz
3 questions. Check what stuck and earn 2 XP for each right answer.
Progress is saved in this browser. Create a free account to keep it on every device.
Get new lessons in your inbox
One email when we publish a new tutorial or guide. No spam, unsubscribe in one click.