Entity Mapping — @Entity, @Id, Inheritance & Composite Keys
Every entity you've used in this course had to be correctly mapped first — this chapter covers that mapping directly, including the genuinely hard cases (inheritance, composite keys) that have no single obvious relational equivalent.
Learning objectives
- Choose the correct inheritance mapping strategy (Single Table, Joined, Table Per Class) for a given subtype shape and query pattern.
- Map a composite primary key correctly using @EmbeddedId, including equals()/hashCode().
- Choose a primary key generation strategy deliberately based on scaling and batching needs.
Every entity you've used in this course had to be correctly mapped first — this chapter is where you actually learn to map one, including the genuinely tricky cases (inheritance, composite keys) that have no single obvious relational equivalent.
📖 Story
Imagine a payment system that needs to model CreditCardPayment, BankTransferPayment, and WalletPayment — three Java subclasses sharing a common Payment base class. Java's inheritance handles this naturally. But a relational table? Tables don't have a native concept of "is-a" at all — you have to CHOOSE a strategy for representing this.
Here's the base entity, using the most common choice — Single Table:
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "payment_type") public abstract class Payment { @Id @GeneratedValue private Long id; private BigDecimal amount; } @Entity @DiscriminatorValue("CREDIT_CARD") public class CreditCardPayment extends Payment { private String last4Digits; } @Entity @DiscriminatorValue("BANK_TRANSFER") public class BankTransferPayment extends Payment { private String iban; }
This generates ONE table (payment) holding every subtype's columns together, with a payment_type column telling Hibernate which subtype each row actually is. Querying "all payments for this order, regardless of type" needs no JOIN at all — it's one table.
@Entity — marks a class as JPA-managed, mapped to a table. @Id/@GeneratedValue — the primary key field, and how its value is generated. @MappedSuperclass — a non-entity base class whose fields are inherited, with no table of its own. @Embeddable/@Embedded — a reusable value-object type (like an Address) stored directly in the owning entity's columns. Composite key — a primary key made of more than one column, mapped via @IdClass or @EmbeddedId.
Let's continue this chapter's payment example and compare all three inheritance strategies concretely.
Single Table — this chapter's opening example
One shared table, a discriminator column. Fastest (no joins, ever), but CreditCardPayment's last4_digits column sits alongside BankTransferPayment's iban column in the SAME table — most rows leave one of the two columns NULL.
Joined — a separate table per subtype
@Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract class Payment { ... } // same base fields, own table @Entity public class CreditCardPayment extends Payment { ... } // its OWN table, joined back by ID
No wasted NULL columns — but reconstructing a full CreditCardPayment now requires Hibernate to JOIN the base payment table with credit_card_payment.
Table Per Class — fully independent tables
Each concrete subclass gets a completely separate table, duplicating the base fields (amount) in each. No joins needed for one subtype — but a query across ALL payment types needs a UNION across every table.
Composite keys — when one column isn't enough
Say you're modeling order_items, naturally keyed by (order_id, product_id) together:
@Embeddable public class OrderItemId implements Serializable { private Long orderId; private Long productId; // must correctly implement equals() and hashCode() using BOTH fields } @Entity public class OrderItem { @EmbeddedId private OrderItemId id; private Integer quantity; }
For Single Table inheritance (this chapter's payment example), Hibernate generates one table with every mapped field from every subtype as a column, plus the discriminator column — a query for one specific subtype adds a WHERE payment_type = '...' filter automatically. For Joined inheritance, each subtype table's primary key ALSO serves as a foreign key back to the base table — reconstructing a full entity means Hibernate JOINs the base table with that specific subtype's table.
- This chapter's
Paymenthierarchy is a textbook Single Table case — polymorphic queries ("all payments for this order, regardless of type") are frequent, and joins would otherwise be needed for every one. - An
order_itemscomposite-key entity, exactly this chapter'sOrderItemIdexample, is extremely common wherever a join-table-style relationship needs its own additional data (likequantity). - A distributed system generating IDs across multiple services before any of them write to a shared database commonly uses UUID primary keys, since they can be generated independently, with no coordination round-trip needed first.
- Default to Single Table (this chapter's example) for closely related subtypes with mostly-shared fields and frequent polymorphic queries.
- Reach for Joined specifically when subtypes have substantially different fields and you want to avoid sparse, mostly-NULL columns.
- Prefer
@EmbeddedIdover@IdClassfor composite keys in new code — it more directly expresses the key as a genuine value object.
⚠️ Why this keeps happening
JPA deliberately offers a CHOICE of inheritance strategies with no single "correct" default — this means teams often pick whichever strategy an example happened to use, without weighing it against their actual subtype shape.
- Choosing Table Per Class without considering polymorphic query cost — a query across all subtypes needs a UNION across every table.
- Choosing Single Table for wildly different subtypes, ending up with a table full of sparse, mostly-empty columns.
- Mapping a composite key without correctly implementing
equals()/hashCode()on ALL its constituent fields — Hibernate's entity identity comparisons break subtly if you don't.
Single Table inheritance (this chapter's example) is fastest for typical CRUD and polymorphic queries — no joins needed at all — but can bloat table size with unused columns at scale. GenerationType.SEQUENCE with a configured allocationSize reduces database round-trips for ID generation compared to IDENTITY, since IDs get pre-allocated in batches.
Using a sequential auto-increment ID as a publicly exposed identifier (in a URL, say) can leak information — roughly how many records exist, or enable enumeration of adjacent IDs. A UUID primary key avoids this specific leak.
Track table sizes and column-nullability rates for Single Table hierarchies over time — a growing number of subtypes with increasingly divergent fields is a signal worth revisiting whether Single Table is still the right choice.
Document the chosen inheritance strategy and reasoning directly near the base entity class — this decision has real, hard-to-reverse schema implications.
- Build this chapter's
Payment/CreditCardPayment/BankTransferPaymenthierarchy using Single Table inheritance, and inspect the single generated table and discriminator column. - Remodel the same hierarchy using Joined inheritance, and compare the generated schema and a polymorphic query's SQL against the Single Table version.
- Create the
OrderItemIdcomposite key from this chapter, correctly implementingequals()/hashCode(), and confirm entity lookups by composite key work.
✓ Quick recap
- Inheritance has three mapping strategies (Single Table, Joined, Table Per Class), each a genuinely different tradeoff — this chapter's
Paymentexample uses Single Table, the most common default. - Composite keys use
@IdClassor@EmbeddedId— the embeddable key class must correctly implementequals()/hashCode()on all its fields. - Choose primary key generation strategy deliberately based on your actual scaling and batching needs.
Want a visual for this concept?
Generate a diagram tailored to “Entity Mapping — @Entity, @Id, Inheritance & Composite Keys” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →