JPA offers two ways to map a composite primary key: @EmbeddedId and @IdClass. How do they differ in mapping, in query syntax, and in when you would choose one over the other?
answer
- EmbeddedId = key is one @Embeddable field
- IdClass = @Id on entity fields + mirror class
- JPQL: l.id.orderId vs l.orderId
- Both: Serializable, no-arg ctor, equals/hashCode
- No @GeneratedValue on composite keys
basics
~20 s@EmbeddedId puts the key in one @Embeddable field, so you access and query it through that field (entity.id.orderId). @IdClass declares the key properties directly on the entity with @Id each, plus a mirror class listing them; queries use flat paths. Both key classes need Serializable, a no-arg constructor and equals/hashCode.
solid answer
~50 sBoth map a multi-column primary key; they differ in where the key attributes live. **@EmbeddedId** — the entity has one field of an `@Embeddable` type holding all key columns. Access is `order.getId().getOrderId()`, and JPQL navigates: `where o.id.orderId = :x`. It is the more object-oriented form, keeps the key as a real type you can pass around and hand to `em.find(Order.class, new OrderId(...))`, and it is required for `@MapsId` when the key is embedded. **@IdClass** — the key attributes are declared on the entity itself, each annotated `@Id`; a separate class (any plain class, not `@Embeddable`) mirrors them by name and type and is named in `@IdClass(OrderId.class)`. JPQL uses flat paths: `where o.orderId = :x`. It suits legacy schemas and mapping-lite code, and it duplicates the key attributes in two places. Both key classes must be public, `Serializable`, have a no-arg constructor and correct `equals`/`hashCode` — Hibernate keys the persistence context and the second-level cache by identifier equality. Modern code generally prefers `@EmbeddedId`.
code
java · 13 lines// @EmbeddedId
@Entity
class OrderLine {
@EmbeddedId private OrderLineId id;
}
// @IdClass
@Entity
@IdClass(OrderLineId.class)
class OrderLine2 {
@Id private Long orderId;
@Id private Long productId;
}go deeper
Know both exist, that they map a multi-column primary key, and that the key class needs equals/hashCode and Serializable.
Contrast the mappings concretely: where attributes live, how find() and JPQL differ, and why composite keys cannot be generated.
Bring in @MapsId for FK-based keys, the duplication risk of @IdClass, immutability of key values, and identity-map/second-level-cache reliance on equals/hashCode.
Argue the codebase-level call: pick one style and enforce it, and question whether the composite key belongs in the model at all versus a surrogate key with a unique constraint.
## Two spellings of the same schema A composite primary key means the table's PK spans two or more columns. JPA gives two mappings. ### @EmbeddedId ```java @Embeddable public class OrderLineId implements Serializable { private Long orderId; private Long productId; protected OrderLineId() {} public OrderLineId(Long orderId, Long productId) { ... } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } } @Entity public class OrderLine { @EmbeddedId private OrderLineId id; private int quantity; } ``` The key is a first-class type. `em.find(OrderLine.class, new OrderLineId(1L, 2L))` reads naturally, the key can be passed through service signatures, and column names are tuned with `@AttributeOverride` on the `@EmbeddedId` field. The cost is a level of indirection everywhere — including in queries: `select l from OrderLine l where l.id.orderId = :o`. ### @IdClass ```java public class OrderLineId implements Serializable { private Long orderId; // names and types must match the entity private Long productId; public OrderLineId() {} @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } } @Entity @IdClass(OrderLineId.class) public class OrderLine { @Id private Long orderId; @Id private Long productId; private int quantity; } ``` The attributes live on the entity, so JPQL is flat (`where l.orderId = :o`) and getters read directly. The id class is only a *lookup token*: it is what you pass to `find()` and what `getReference()` needs, but it is never populated as an entity field. The obvious downside is duplication — change a key attribute and you must change it in two files, with no compiler check that they still match. A mismatch in name or type surfaces as a bootstrap error or, worse, as odd behaviour. ## Rules that apply to both - The key class must be **public**, implement **`Serializable`**, have a **no-arg constructor**, and implement **`equals`/`hashCode`** over all key attributes. Hibernate uses identifier equality to key the first-level cache (the persistence context map), the second-level cache, and to resolve `merge`/`find`. - Key attributes must be basic types, other embeddables, or (with derived identity) associations. Composite keys **cannot** use `@GeneratedValue`; you assign the values yourself or derive them from associations via `@MapsId`. - Key attributes should be treated as immutable. Changing a key value after persist means Hibernate's identity map and the database row disagree; the safe operation is delete-and-insert. - `@AttributeOverride` works on `@EmbeddedId` to rename key columns per entity; with `@IdClass` you simply annotate the entity fields with `@Column`. ## Derived identity When the composite key is made of foreign keys — the common case for link tables — both forms support `@MapsId`. With `@EmbeddedId` you write `@MapsId("orderId") @ManyToOne Order order`, and Hibernate keeps the key field and the association's FK column in sync from a single mapping. With `@IdClass`, you can instead annotate the association itself `@Id @ManyToOne Order order`, and the id class declares the *type of the FK* (or of the parent's id) for that name. ## Choosing Prefer `@EmbeddedId` for new code: one source of truth for the key, a reusable value type, natural `find()` calls, and first-class support for `@MapsId`. Reach for `@IdClass` when you are mapping a legacy schema where flat entity attributes and flat JPQL matter, when a framework or query layer you use dislikes nested id paths, or when you want the key attributes to be plain fields on the entity for readability. Functionally the two produce the same SQL; the choice is ergonomic, and consistency across a codebase matters more than which one you pick.
- Can a composite key use @GeneratedValue for one of its parts?No. JPA does not define generation for composite identifiers, so `@GeneratedValue` on a key part of an `@EmbeddedId` or `@IdClass` is not portable and Hibernate will not populate it. The values are either assigned by the application before `persist`, or derived from associations with `@MapsId`. If you truly need generation, the usual answer is a surrogate single-column key plus a unique constraint on the business columns.
- How do you write a JPQL query filtering on one part of the key for each mapping style?With `@EmbeddedId` you navigate into the id attribute: `select l from OrderLine l where l.id.orderId = :o`. With `@IdClass` the attributes are on the entity, so it is flat: `select l from OrderLine2 l where l.orderId = :o`. You can also compare whole keys in either style by binding a key instance: `where l.id = :key`.
- Does the @IdClass have to be annotated @Embeddable?No. The id class named in `@IdClass` is a plain class — it must be public, `Serializable`, have a no-arg constructor and implement `equals`/`hashCode`, with field names and types matching the entity's `@Id` attributes. Annotating it `@Embeddable` is harmless but unnecessary; that annotation is what an `@EmbeddedId` type requires.
saying these in an interview costs you the question
- Claiming @IdClass and @EmbeddedId generate different SQL or different schemas
- Forgetting Serializable, the no-arg constructor, or equals/hashCode on the key class
- Expecting @GeneratedValue to fill part of a composite key
- Using flat JPQL paths with @EmbeddedId (l.orderId instead of l.id.orderId)
- Letting the @IdClass field names drift from the entity's @Id fields