skip to content

How does @MapsId let a JPA @OneToOne share a primary key with its owner, and what problem does that solve compared with a plain @OneToOne @JoinColumn?

level: seniorimportance: should knowfreq 42%

answer

  1. @MapsId: child's PK is the FK to the parent
  2. @Id field has no @GeneratedValue
  3. One column, one index, cardinality enforced by PK
  4. Inverse @OneToOne can't be lazy without shared PK + optional=false
  5. Derived identity → cannot reassign to another owner

basics

~20 s

@MapsId makes the child's primary key be the foreign key to the parent — one column doing both jobs. It removes the separate surrogate key and unique constraint, and it fixes the lazy one-to-one problem on the parent side by making the child's ID known in advance.

solid answer

~50 s

With `@MapsId`, the dependent entity's primary key **is** the foreign key to its owner: ```java @Entity class UserProfile { @Id private Long userId; @MapsId @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; } ``` One column, `user_id`, is both PK and FK. That gives uniqueness for free (a PK is unique), removes a surrogate key and a separate unique index, and makes the one-to-one physically impossible to violate. The deeper win is on the **parent side**. A plain optional `@OneToOne(mappedBy=...)` cannot be lazy: Hibernate has to query the child table just to learn whether a row exists, so it eagerly selects it even when you declared `LAZY`. With a shared PK the parent already knows the child's identifier — it equals its own — and with `optional = false` it can return a proxy without hitting the database. Cost: the child's identity is now derived, so you must set the association before persisting, and the child cannot be re-pointed at another owner.

code

java · 19 lines
java
@Entity
class User {
  @Id @GeneratedValue private Long id;
  @OneToOne(mappedBy = "user", cascade = CascadeType.ALL,
            fetch = FetchType.LAZY, optional = false)
  private UserProfile profile;
}

@Entity
class UserProfile {
  @Id private Long userId;                 // no @GeneratedValue

  @MapsId
  @OneToOne(fetch = FetchType.LAZY, optional = false)
  @JoinColumn(name = "user_id")
  private User user;

  private String bio;
}

go deeper

for a junior

Recall that @MapsId makes the child's primary key the same value as the parent's, so one column is both PK and FK.

for a middle

Contrast the two schemas, note that the @Id field must not be generated, and explain that the association must be set before persist.

for a senior

Lead with the lazy inverse one-to-one problem and why shared PK plus optional = false is what actually makes it lazy; cover the no-reassignment constraint.

for a principal

Frame it as identity modelling — is the child a dependent extension or an independent entity? — and connect the choice to lifecycle, referencing tables, generation strategy and query cost.

## Two ways to map a one-to-one **Plain FK one-to-one:** the child has its own generated primary key plus a foreign-key column to the parent, and you add a unique constraint so it stays one-to-one. ```sql create table user_profile ( id bigint primary key, user_id bigint not null unique references users(id), bio text ); ``` **Shared primary key:** the child has no separate identity at all. Its primary key *is* the parent's key. ```sql create table user_profile ( user_id bigint primary key references users(id), bio text ); ``` The second is strictly smaller: one column instead of two, one index instead of two, and the one-to-one cardinality is enforced by the primary key rather than by a constraint someone must remember to add. ## The @MapsId mapping ```java @Entity class User { @Id @GeneratedValue private Long id; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private UserProfile profile; } @Entity class UserProfile { @Id private Long userId; // no @GeneratedValue @MapsId // this association supplies the @Id value @OneToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id") private User user; private String bio; } ``` `@MapsId` says: **derive the identifier from this association.** The `@Id` field is not generated; Hibernate copies the parent's id into it at flush time. Note the `@Id` field carries no `@GeneratedValue` — adding one is a common error that produces a duplicate-key or a mismatched id. When the child key is composite, `@MapsId("attributeName")` names which part of an `@EmbeddedId` the association fills — the same mechanism used for link entities in many-to-many promotions. ## The lazy one-to-one problem This is the reason a senior candidate should know `@MapsId` beyond schema tidiness. On the **owning** side (the one with the FK column), lazy works: the FK value is in the row, so Hibernate can build a proxy keyed on it without querying. On the **inverse** (`mappedBy`) side, the parent's row contains nothing about the child. To create a proxy Hibernate would need the child's identifier, and to know whether the field should be `null` it must know whether a child row exists at all. Since a proxy cannot represent "absent", Hibernate has no choice but to issue a secondary select immediately — the association is effectively eager no matter what `fetch = LAZY` says. Every load of a `User` costs a second query. A shared primary key changes the arithmetic: the child's identifier is the parent's identifier, already in hand. If the association is also declared `optional = false` (a profile always exists), Hibernate may safely return a proxy without checking — nullability was the only remaining unknown. So `@MapsId` + `optional = false` is the combination that actually delivers a lazy inverse one-to-one. The alternatives when you cannot use a shared PK are bytecode enhancement with `@LazyToOne`-style enhanced proxies, or simply not mapping the inverse side and querying the child when you need it. ## Write-time semantics Because identity is derived, ordering matters: ```java User u = new User("ada"); em.persist(u); // id generated here UserProfile p = new UserProfile(); p.setUser(u); // MUST be set — supplies the id p.setBio("..."); em.persist(p); // no id generation; uses u.getId() ``` Persisting the profile without setting `user` fails: there is no identifier to assign. With `cascade = ALL` from the parent and both sides wired, a single `persist(user)` handles it. You also cannot **move** a profile to another user — that would change its primary key, which is an identity change, not an update. If reassignment is a real requirement, the shared-PK model is the wrong model and you want the plain FK with a unique constraint. ## When to use which Use **shared PK / `@MapsId`** when the child is a *dependent extension* of the parent with no independent life: profile, settings, detail rows, an attribute table split off for LOB isolation. Benefits: smaller schema, guaranteed cardinality, lazy inverse side, cheaper joins (PK to PK). Use a **plain FK + unique constraint** when the child has independent identity, can be reassigned, may exist before the parent, or is referenced by other tables under its own key. One caveat worth naming: with `@MapsId` the parent must be persisted (or at least have its id assigned) before the child's identifier is known, which interacts with identity-column generation strategies that only assign on insert. With `SEQUENCE` generation the id is available earlier, which makes batching more predictable.

  • Why is a mappedBy @OneToOne effectively eager even when declared LAZY, and how does a shared primary key change that?
    The parent's row holds no information about the child, so Hibernate cannot know the child's identifier or even whether a child row exists — and a proxy cannot represent null. It therefore issues a secondary select on every parent load. With @MapsId the child's identifier equals the parent's, so it is already known; combined with optional = false, which removes the existence question, Hibernate can hand back a proxy without querying.
  • What breaks if you put @GeneratedValue on the @Id field of a @MapsId child?
    The identifier is supposed to be derived from the association, so a generator competes with that: you either get an id that does not match the parent's, or a duplicate-key failure when Hibernate later assigns the derived value. The @Id field must be a plain field that @MapsId populates at flush time.
  • When is a plain @OneToOne with a unique foreign key the better choice?
    When the child has independent identity — it can exist before the parent, be reassigned to a different parent, or be referenced by other tables under its own key. Reassignment is the decisive test: under @MapsId, changing the owner would mean changing the child's primary key, which is an identity change rather than an update.

A shared primary key is an apartment number that is also the mailbox number — one identifier serving both roles, so they can never disagree.

saying these in an interview costs you the question

  • Adding @GeneratedValue to the @Id field of a @MapsId entity
  • Believing a mappedBy @OneToOne is lazy just because it is annotated LAZY
  • Expecting @MapsId to allow reassigning the child to a different parent
  • Persisting the dependent entity without setting the association that supplies its id
  • Using a shared primary key for a child that has independent lifecycle or is referenced elsewhere
  • Omitting the unique constraint in the plain-FK alternative and calling it a one-to-one

context