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