skip to content

A link entity's primary key is (order_id, product_id) and the same entity also needs @ManyToOne references to Order and Product. How do you map that in JPA/Hibernate without mapping the same two columns twice?

level: seniorimportance: should knowfreq 38%

answer

  1. Repeated column in mapping → needs @MapsId
  2. @MapsId("orderId") on the @ManyToOne
  3. Association writes the column, id field derives
  4. @IdClass variant: @Id @ManyToOne
  5. @MapsId alone = shared-PK @OneToOne

basics

~20 s

Use derived identity: keep an @EmbeddedId holding the two id values and annotate each association with @MapsId("orderId") / @MapsId("productId"). Hibernate then owns the columns once through the associations and fills the key fields from them, so you set only the associations.

solid answer

~50 s

Mapping the FK columns twice — once in the `@EmbeddedId` and once in the `@ManyToOne` — makes Hibernate fail with a repeated-column error. JPA's answer is **derived identity** via `@MapsId`: ```java @Entity class OrderLine { @EmbeddedId private OrderLineId id; @MapsId("orderId") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private Order order; @MapsId("productId") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; } ``` `@MapsId("orderId")` says: this association *is* the source of the `orderId` attribute of the embedded key. You construct the entity by setting the associations (the id fields are populated at persist time), and you can still `find()` by a fully built `OrderLineId`. The alternative — keeping both mappings and marking the association `insertable = false, updatable = false` — works but leaves two mappings to keep in sync manually. With `@IdClass` the equivalent is `@Id @ManyToOne Order order`. The same mechanism gives shared-primary-key `@OneToOne`: `@MapsId @OneToOne Order` with a plain `@Id Long id`.

code

java · 19 lines
java
@Embeddable
record OrderLineId(Long orderId, Long productId) implements Serializable {}

@Entity
class OrderLine {
    @EmbeddedId private OrderLineId id;

    @MapsId("orderId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @MapsId("productId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    private int quantity;
}

go deeper

for a junior

Recognise the repeated-column error and know that @MapsId links an association to a part of the composite key.

for a middle

Write the mapping correctly, explain that the association becomes the writable side and the key is derived at persist time, and keep the associations lazy.

for a senior

Compare @MapsId with the insertable=false/updatable=false workaround, cover the @IdClass form and shared-PK @OneToOne, and discuss persist ordering with generated parent ids.

for a principal

Weigh derived composite keys against a surrogate key with a unique constraint for link entities that accumulate their own lifecycle, and set a house rule so the codebase does not mix both patterns.

## The collision A join/link table typically has a primary key made of its foreign keys: ```sql CREATE TABLE order_line ( order_id BIGINT NOT NULL REFERENCES orders(id), product_id BIGINT NOT NULL REFERENCES product(id), quantity INT NOT NULL, PRIMARY KEY (order_id, product_id) ); ``` The naive mapping declares `@EmbeddedId OrderLineId(orderId, productId)` *and* two `@ManyToOne` associations on `order_id` / `product_id`. Two mappings now claim the same columns, and Hibernate refuses at bootstrap: `Repeated column in mapping for entity`. ## Derived identity with @MapsId JPA calls a key whose parts come from associations a **derived identity**. `@MapsId("attributeName")` on an association declares that the association owns the column and supplies the value of that attribute in the embedded id. What this buys you: - **One writable mapping per column.** The association is the writable side; the id attribute is filled from it. - **Natural construction.** `new OrderLine(order, product, 3)` — you never assign the raw ids by hand; at `persist` Hibernate reads the parents' identifiers into the key. (The parents must already have identifiers, so persist them first or let cascade order handle it.) - **Lookup still works by key.** `em.find(OrderLine.class, new OrderLineId(1L, 2L))` is unchanged. - **Lazy parents.** The associations can be `LAZY` like any other `@ManyToOne`; the key values are available without initialising the proxies because they are stored in the id itself. The `@JoinColumn` name must line up with the column you want; `@AttributeOverride` on the `@EmbeddedId` renames the key columns when defaults do not match the schema. ## The variants **@IdClass style.** Annotate the associations themselves as identifiers: ```java @Entity @IdClass(OrderLineId.class) class OrderLine { @Id @ManyToOne @JoinColumn(name = "order_id") private Order order; @Id @ManyToOne @JoinColumn(name = "product_id") private Product product; } ``` The id class then declares `Long orderId; Long productId;` — attributes named after the association but typed as the *parent's identifier type*. **Shared primary key @OneToOne.** The same annotation gives the one-table-extension pattern: ```java @Entity class OrderDetails { @Id private Long id; // same value as the order's id @MapsId @OneToOne @JoinColumn(name = "id") private Order order; } ``` Here `@MapsId` with no argument maps the whole (simple) identifier. This is the recommended way to model a 1:1 extension: one column, no extra unique index, and the child's PK is also its FK. **Read-only duplicate mapping.** Without `@MapsId` you can keep both mappings and mark the association `@JoinColumn(name="order_id", insertable = false, updatable = false)`. It compiles and reads correctly, but writes go only through the id fields, so you must set the raw ids yourself and the object graph can be inconsistent in memory until you reload. Prefer `@MapsId`. ## Practical notes - The parent's identifier must be assigned before the child is flushed. With `IDENTITY` generation on the parent, the insert happens at `persist` time, so the ordinary order (persist parent, then child) works. - Do not also add setters that mutate the key fields directly — the id is derived; the association is the source of truth. - Deleting a link row is a plain `remove` on the link entity; `@MapsId` does not imply cascade in either direction. - If the link entity later grows its own attributes and lifecycle (surrogate id, audit columns, its own soft-delete), that is often a signal to give it a surrogate key and a unique constraint on the pair instead. ## Why interviewers ask It separates people who have only mapped `@Id Long id` entities from people who have modelled association tables with payload — a very common real schema — and it exercises understanding of who owns a column in a Hibernate mapping.

  • With @MapsId, do you still set the id fields yourself before persisting?
    No — you set the associations and Hibernate derives the key values from the parents' identifiers when the entity is persisted. That is the point of derived identity: one writable mapping per column. You may of course construct a key instance to call `find()`, but assigning key fields on a managed entity is wrong because the association is the source of truth.
  • What is the difference between @MapsId and marking the association insertable=false, updatable=false?
    Both resolve the repeated-column conflict, but the read-only variant leaves the id attributes as the writable side: you must assign raw identifier values yourself, and the in-memory association can disagree with them until you refresh. `@MapsId` inverts that — the association is writable and the key is derived — which keeps the object graph consistent and is the JPA-blessed form of derived identity.

saying these in an interview costs you the question

  • Mapping the FK columns both in the @EmbeddedId and in a writable @ManyToOne
  • Believing @MapsId forces the parent association to be EAGER
  • Setting the id fields manually on an entity that uses @MapsId
  • Thinking @MapsId implies cascading persist or remove
  • Confusing @MapsId with @PrimaryKeyJoinColumn semantics for inheritance

context