Careeroza
SINGLE_TABLE
One Table for the Whole Hierarchy
ShopEase accepts three payment methods: card, UPI and cash on delivery. Each is a Payment with its own extra fields. With SINGLE_TABLE (the JPA default) all of them live in one table, and a discriminator column tells Hibernate which class each row is.
@Entity
@Table(name = "payments")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING, length = 10)
public abstract class Payment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal amount;
@OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id")
private Order order;
}
@Entity @DiscriminatorValue("CARD")
public class CardPayment extends Payment { private String cardLast4; private String network; }
@Entity @DiscriminatorValue("UPI")
public class UpiPayment extends Payment { private String upiId; }
@Entity @DiscriminatorValue("COD")
public class CodPayment extends Payment { private boolean collected; }The Table and the Rows
Example
Each subclass uses only its own columns; the others are NULL. This program prints the rows the way they sit in the single table:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Payment> payments = List.of(
new Card(1, 1798, "4242", "VISA"),
new Upi(2, 349, "asha@okhdfc"),
new Cod(3, 1299, false),
new Upi(4, 2499, "ravi@ybl"));
System.out.println("payments");
System.out.printf("%-3s %-12s %-7s %-10s %-8s %-12s %-9s%n",
"id", "payment_type", "amount", "card_last4", "network", "upi_id", "collected");
for (Payment p : payments) {
String[] row = switch (p) {
case Card c -> new String[] {"CARD", c.last4(), c.network(), "NULL", "NULL"};
case Upi u -> new String[] {"UPI", "NULL", "NULL", u.upiId(), "NULL"};
case Cod d -> new String[] {"COD", "NULL", "NULL", "NULL", String.valueOf(d.collected())};
};
System.out.printf("%-3d %-12s %-7d %-10s %-8s %-12s %-9s%n",
p.id(), row[0], p.amount(), row[1], row[2], row[3], row[4]);
}
}
}
sealed interface Payment permits Card, Upi, Cod {
long id();
int amount();
}
record Card(long id, int amount, String last4, String network) implements Payment {}
record Upi(long id, int amount, String upiId) implements Payment {}
record Cod(long id, int amount, boolean collected) implements Payment {}Output
payments id payment_type amount card_last4 network upi_id collected 1 CARD 1798 4242 VISA NULL NULL 2 UPI 349 NULL NULL asha@okhdfc NULL 3 COD 1299 NULL NULL NULL false 4 UPI 2499 NULL NULL ravi@ybl NULL
Queries Are Simple and Fast
-- select p from Payment p where p.amount > 1000 (polymorphic: no join, no union)
select p1_0.id,p1_0.payment_type,p1_0.amount,p1_0.card_last4,p1_0.network,p1_0.collected,p1_0.order_id,p1_0.upi_id
from payments p1_0 where p1_0.amount>?
-- select u from UpiPayment u (filter on the discriminator)
select u1_0.id,u1_0.amount,u1_0.order_id,u1_0.upi_id from payments u1_0 where u1_0.payment_type='UPI'- Pros: fastest polymorphic queries, simplest schema, easy to add a subclass.
- Cons: subclass columns must be nullable (you cannot put NOT NULL on
upi_id); many subclasses make a wide, sparse table.
Tip: Enforce subclass rules with Bean Validation on the entity (@NotBlank String upiId) or a database CHECK constraint such as check (payment_type <> 'UPI' or upi_id is not null).