Tools
Tools: JPA Mapping with Hibernate-One-to-One Relationship
2026-03-07
0 views
admin
JPA (Java Persistence API) ## Hibernate ## Relationship Between JPA and Hibernate ## Relationship Mapping in JPA ## Key Best Practices ## One-to-One Relationship Mapping in JPA ## Best Way to Map a @OneToOne Relationship Using @MapsId ## Key Differences Per Entity ## Summary ## Why @MapsId Solves It ## Advantages of Using @MapsId ## Downsides of Using @MapsId ## When @MapsId Is Ideal The Java Persistence API (JPA) is a Java specification that defines a standard way to manage relational data in Java applications using Object Relational Mapping (ORM). It provides a set of interfaces and annotations that allow developers to map Java objects to database tables, perform CRUD operations, and manage persistence without writing large amounts of SQL. Hibernate is a popular open-source ORM framework that provides the implementation of the JPA specification. It allows developers to interact with databases using Java objects instead of writing complex SQL queries, making the application code loosely coupled with the underlying database. A One-to-One relationship occurs when one entity is associated with exactly one instance of another entity. The User entity is the parent, while the UserProfile is the child association because the Foreign Key is located in the 'user_profile' database table. This relationship is mapped as follows: UserProfile.java — OWNING side (holds the FK) The user_profile table contains a Primary Key (PK) column (e.g. id) and a Foreign Key (FK) column (e.g. user_id). User.java — the INVERSE side @ElementCollection stores simple values (Strings) in a separate table — not a full entity relationship, so there's no "other side" to sync. Specifying FetchType.LAZY for the non-owning side of the @OneToOne association will not affect the loading. On the inverse side (mappedBy), Hibernate doesn't know if the associated entity exists or not without hitting the DB. which defeats the purpose of LAZY. So it just loads eagerly regardless of what you specify.
On the owning side, this problem doesn't exist — the FK column is right there in the same row, so Hibernate knows immediately if it's null or not, and can safely create a proxy. Query 1: select from users ← your actual request
Query 2: select from user_profile ← YOU DIDN'T ASK FOR THIS
Query 3: select from user_roles ← expected (EAGER @ElementCollection) Query 2 is Hibernate silently probing — "does a profile exist for this user?" — because profile is on the inverse side and has no FK column to check. Query 3 (user_roles) firing is expected and correct — @ElementCollection(fetch = FetchType.EAGER) is explicitly eager. If you don't need roles on every fetch, change it:
@ElementCollection(fetch = FetchType.LAZY) // load roles only when needed
Since I am using Spring Security, keeping it EAGER — UserDetails needs roles immediately on authentication. Option 1: @LazyToOne — Bytecode Instrumentation (True Lazy) // User.java
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY) // forces true lazy via bytecode
private UserProfile profile; Requires adding the Hibernate bytecode enhancer to your build: Option 2: @MapsId — Shared Primary Key (Best & Simplest) The best way to map a @OneToOne relationship in Hibernate is by using @MapsId, which allows the child entity to share the same primary key as the parent entity. For example, in a User and UserProfile relationship: For JOIN FETCH to work, User must have the profile field back — bidirectional. Insert Order Dependency Harder to Manage Independent Lifecycle Not Suitable for Optional Relationships More Complex for Beginners Developers unfamiliar with JPA may find: slightly harder to understand compared to simple FK mapping. Migration / Schema Changes Are Harder Use it when:
the child entity is just an extension of the parent entity and shares the same lifecycle. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String phone; @Column(nullable = false) private String address; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", unique = true) private User user; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String phone; @Column(nullable = false) private String address; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", unique = true) private User user; } CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String phone; @Column(nullable = false) private String address; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", unique = true) private User user; } COMMAND_BLOCK:
@Getter
@Setter
@Entity
@Table(name = "users")
public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private boolean enabled = true; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private UserProfile profile; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"})) @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) private Set<Role> roles = new HashSet<>(); public void addRole(Role role) { if (role != null) { this.roles.add(role); } } public void removeRole(Role role) { this.roles.remove(role); } public boolean hasRole(Role role) { return this.roles.contains(role); } public void setProfile(UserProfile profile) { this.profile = profile; if (profile != null) { profile.setUser(this); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User user)) return false; return username != null && username.equals(user.getUsername()); } @Override public int hashCode() { return getClass().hashCode(); }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
@Getter
@Setter
@Entity
@Table(name = "users")
public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private boolean enabled = true; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private UserProfile profile; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"})) @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) private Set<Role> roles = new HashSet<>(); public void addRole(Role role) { if (role != null) { this.roles.add(role); } } public void removeRole(Role role) { this.roles.remove(role); } public boolean hasRole(Role role) { return this.roles.contains(role); } public void setProfile(UserProfile profile) { this.profile = profile; if (profile != null) { profile.setUser(this); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User user)) return false; return username != null && username.equals(user.getUsername()); } @Override public int hashCode() { return getClass().hashCode(); }
} COMMAND_BLOCK:
@Getter
@Setter
@Entity
@Table(name = "users")
public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private boolean enabled = true; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private UserProfile profile; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"})) @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) private Set<Role> roles = new HashSet<>(); public void addRole(Role role) { if (role != null) { this.roles.add(role); } } public void removeRole(Role role) { this.roles.remove(role); } public boolean hasRole(Role role) { return this.roles.contains(role); } public void setProfile(UserProfile profile) { this.profile = profile; if (profile != null) { profile.setUser(this); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User user)) return false; return username != null && username.equals(user.getUsername()); } @Override public int hashCode() { return getClass().hashCode(); }
} CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id //no @GeneratedValue — inherited from User
private Long id; @Column(nullable = false)
private String phone; @Column(nullable = false)
private String address; @OneToOne(fetch = FetchType.LAZY)
@MapsId // tells Hibernate: this entity's PK = users.id
@JoinColumn(name = "id")
private User user; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id //no @GeneratedValue — inherited from User
private Long id; @Column(nullable = false)
private String phone; @Column(nullable = false)
private String address; @OneToOne(fetch = FetchType.LAZY)
@MapsId // tells Hibernate: this entity's PK = users.id
@JoinColumn(name = "id")
private User user; CODE_BLOCK:
@Entity
@Table(name = "user_profile")
@Getter
@Setter
public class UserProfile { @Id //no @GeneratedValue — inherited from User
private Long id; @Column(nullable = false)
private String phone; @Column(nullable = false)
private String address; @OneToOne(fetch = FetchType.LAZY)
@MapsId // tells Hibernate: this entity's PK = users.id
@JoinColumn(name = "id")
private User user; COMMAND_BLOCK:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.profile WHERE u.id = :id")
Optional<User> findByIdWithProfile(@Param("id") Long id); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.profile WHERE u.id = :id")
Optional<User> findByIdWithProfile(@Param("id") Long id); COMMAND_BLOCK:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.profile WHERE u.id = :id")
Optional<User> findByIdWithProfile(@Param("id") Long id); CODE_BLOCK:
save(User)
save(UserProfile) CODE_BLOCK:
save(User)
save(UserProfile) - JPA is a specification, not an implementation
- It standardizes how Java applications interact with relational databases
- It uses annotations and configuration to map objects to tables
- It simplifies database operations through entity management and persistence context - Hibernate is a JPA implementation
- Provides powerful ORM capabilities
- Handles CRUD operations automatically
- Supports caching, lazy loading, and transaction management
- Reduces boilerplate JDBC code - JPA → Specification (standard API)
- Hibernate → Implementation of that specification - Developers write code using JPA annotations and interfaces
- Hibernate executes the actual database operations - Always use FetchType.LAZY on relationships — avoids N+1 query problem
- mappedBy on the non-owning side (the side without the FK column)
- Cascade carefully — only cascade from parent to child (Order → OrderItem), not upward
- @JoinColumn explicitly names your FK column for clarity
- Snapshot prices in OrderItem.unitPrice — never rely on current Product.price
- Use Set instead of List for @ManyToMany to avoid duplicate join queries - Person → Passport
- User → Profile
- Order → Invoice - @ElementCollection stores simple values (Strings) in a separate table — not a full entity relationship, so there's no "other side" to sync.
- Specifying FetchType.LAZY for the non-owning side of the @OneToOne association will not affect the loading. On the inverse side (mappedBy), Hibernate doesn't know if the associated entity exists or not without hitting the DB. which defeats the purpose of LAZY. So it just loads eagerly regardless of what you specify.
On the owning side, this problem doesn't exist — the FK column is right there in the same row, so Hibernate knows immediately if it's null or not, and can safely create a proxy. - The child table uses the parent’s primary key as its own primary key.
- This avoids creating an additional foreign key column.
- It also simplifies fetching, since the child entity can always be retrieved using the parent’s identifier. - Each User has exactly one UserProfile
- The UserProfile shares the same primary key as the User - The id column in UserProfile serves as both PK and FK — its value is copied directly from User.id. - User generates its own PK and knows nothing about UserProfile. UserProfile borrows User's PK via @MapsId — clean, no extra column, no probe query.
- Removing profile from User.java made it unidirectional.
- Bidirectional = convenience of user.getProfile() but costs an extra query. Unidirectional = no extra query but you lose navigation from User side. Bytecode enhancement is the only way to have both.
- For most apps — skip the plugin, go unidirectional, and use JOIN FETCH when you need both. You get zero runtime cost, zero probe query, and full control over when profile loads. - No extra foreign key column
- Better database normalization**
- More efficient joins
- No need for a bidirectional association
- The UserProfile can always be fetched using the User ID - Tight Coupling Between Entities The child entity (UserProfile) cannot exist without the parent (User) because it shares the same primary key.
This makes the relationship very tightly coupled.
- The child entity (UserProfile) cannot exist without the parent (User) because it shares the same primary key.
- This makes the relationship very tightly coupled.
- Insert Order Dependency The parent entity must be persisted first.
Only then can the child entity be created because it needs the parent's ID. Example flow: save(User)
save(UserProfile)
- The parent entity must be persisted first.
- Only then can the child entity be created because it needs the parent's ID.
- Less Flexibility If in the future the relationship changes from one-to-one → one-to-many, the schema must be redesigned.
A separate foreign key mapping would be easier to extend.
- If in the future the relationship changes from one-to-one → one-to-many, the schema must be redesigned.
- A separate foreign key mapping would be easier to extend.
- Harder to Manage Independent Lifecycle Since the child shares the same primary key, managing it independently becomes difficult.
For example: Deleting User automatically invalidates UserProfile.
- Since the child shares the same primary key, managing it independently becomes difficult.
- For example: Deleting User automatically invalidates UserProfile.
- Deleting User automatically invalidates UserProfile.
- Not Suitable for Optional Relationships If the relationship is optional, @MapsId may not be ideal.
Sometimes a User might exist without a UserProfile.
- If the relationship is optional, @MapsId may not be ideal.
- Sometimes a User might exist without a UserProfile.
- More Complex for Beginners Developers unfamiliar with JPA may find: @MapsId
shared primary key entity lifecycle slightly harder to understand compared to simple FK mapping.
- Developers unfamiliar with JPA may find: @MapsId
shared primary key entity lifecycle slightly harder to understand compared to simple FK mapping.
- shared primary key
- entity lifecycle slightly harder to understand compared to simple FK mapping.
- Migration / Schema Changes Are Harder If you later need to add a separate primary key to the child table, it requires database migration and entity refactoring.
- If you later need to add a separate primary key to the child table, it requires database migration and entity refactoring. - The child entity (UserProfile) cannot exist without the parent (User) because it shares the same primary key.
- This makes the relationship very tightly coupled. - The parent entity must be persisted first.
- Only then can the child entity be created because it needs the parent's ID. - If in the future the relationship changes from one-to-one → one-to-many, the schema must be redesigned.
- A separate foreign key mapping would be easier to extend. - Since the child shares the same primary key, managing it independently becomes difficult.
- For example: Deleting User automatically invalidates UserProfile.
- Deleting User automatically invalidates UserProfile. - Deleting User automatically invalidates UserProfile. - If the relationship is optional, @MapsId may not be ideal.
- Sometimes a User might exist without a UserProfile. - Developers unfamiliar with JPA may find: @MapsId
shared primary key entity lifecycle slightly harder to understand compared to simple FK mapping.
- shared primary key
- entity lifecycle slightly harder to understand compared to simple FK mapping. - shared primary key
- entity lifecycle slightly harder to understand compared to simple FK mapping. - If you later need to add a separate primary key to the child table, it requires database migration and entity refactoring. - The relationship is strictly one-to-one
- The child cannot exist without the parent
- The child is more like an extension of the parent
how-totutorialguidedev.toaidatabase