DEV Community

Cover image for JPA Mapping with Hibernate-One-to-One Relationship
AnkitDevCode
AnkitDevCode

Posted on

JPA Mapping with Hibernate-One-to-One Relationship

JPA (Java Persistence API)

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.

Key points:

  • 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

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.

Key points:

  • Hibernate is a JPA implementation
  • Provides powerful ORM capabilities
  • Handles CRUD operations automatically
  • Supports caching, lazy loading, and transaction management
  • Reduces boilerplate JDBC code

Relationship Between JPA and Hibernate

  • JPA → Specification (standard API)
  • Hibernate → Implementation of that specification

In practice:

  • Developers write code using JPA annotations and interfaces
  • Hibernate executes the actual database operations

Relationship Mapping in JPA

Key Best Practices

  1. Always use FetchType.LAZY on relationships — avoids N+1 query problem
  2. mappedBy on the non-owning side (the side without the FK column)
  3. Cascade carefully — only cascade from parent to child (Order → OrderItem), not upward
  4. @JoinColumn explicitly names your FK column for clarity
  5. Snapshot prices in OrderItem.unitPrice — never rely on current Product.price
  6. Use Set instead of List for @ManyToMany to avoid duplicate join queries

One-to-One Relationship Mapping in JPA

A One-to-One relationship occurs when one entity is associated with exactly one instance of another entity.

One-to-One: Unidirectional vs Bidirectional

Unidirectional

  • Only one entity has a reference to the other. Navigation works in one direction only.

Bidirectional

  • Both entities have a reference to each other. Navigation works in both directions.

How to Identify Owner, FK Side, and Child?

  • The entity that CANNOT exist without the other = child = owns the FK
  • The entity that EXISTS independently = parent = no FK

Questions to Ask

  • WHO cannot exist without the other? → that's the CHILD → holds the FK
  • WHO exists independently? → that's the PARENT → has mappedBy
  • WHO makes sense to delete first? → that's the CHILD → cascade from parent

JPA Annotation Summary

Annotation Side FK
@JoinColumn Owning / Child FK lives here
mappedBy Inverse / Parent no FK
@JoinTable @ManyToMany owner junction table
cascade Parent → Child delete parent = delete child

Child = cannot exist without parent = has @JoinColumn (FK).
Parent = exists independently = has mappedBy (no FK).

Example:

  • Person → Passport
  • User → Profile
  • Order → Invoice

Example:

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) @JoinColumn annotation is used in the owning side - the child entity that contains the foreign key points to the primary key of the parent entity.

@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

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

@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
  • @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.

What's Happening

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 EAGERUserDetails 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:

How it works:

  • Without enhancement → Hibernate probes DB to check if profile is null or not
  • With enhancement → Hibernate injects interceptor into bytecode, no probe needed

Option 2 — JOIN FETCH Query

  • Load both entities together in a single SQL query — profile is already in memory, no probe needed.
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.profile")
    List<User> findAllWithProfile();
Enter fullscreen mode Exit fullscreen mode

turns into

SELECT
    u1_0.id,
    u1_0.enabled,
    u1_0.password,
    u1_0.username,
    p1_0.id,
    p1_0.address,
    p1_0.phone
FROM users u1_0
LEFT JOIN user_profile p1_0
    ON u1_0.id = p1_0.user_id
Enter fullscreen mode Exit fullscreen mode

How it works:

  • Without JOIN FETCH → select from users + select from user_profile (2 queries)
  • With JOIN FETCH → select from users LEFT JOIN user_profile (1 query)

Option 3: @MapsId — Shared Primary Key (Best & Simplest)
Since both entities share the same PK, Hibernate already knows the profile ID without probing.


Map a @OneToOne Relationship Using @MapsId

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.

In this approach:

  • 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.

For example, in a User and UserProfile relationship:

  • Each User has exactly one UserProfile
  • The UserProfile shares the same primary key as the User
@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

So in summary

  • The id column in UserProfile serves as both PK and FK — its value is copied directly from User.id.

Key Differences Per Entity

User UserProfile
@GeneratedValue yes — generates own PK no — copies from User
Owns the relationship no profile field yes — has @MapsId
Probe query on fetch gone — no inverse field
Save strategy userRepository.save(user) userProfileRepository.save(profile)

Summary

  • 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.

Why @MapsId Solves It

Before (@JoinColumn on profile) After (@MapsId)
Profile FK user_profile.user_id user_profile.id = users.id
Hibernate knows ID? Must probe DB Already has it
Extra query on getUser? Always fires Only when accessed
DB schema Extra user_id column Shared PK, cleaner

Advantages of Using @MapsId

  • 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

Downsides of Using @MapsId

  1. 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.
  2. 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)
    
  3. 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.
  4. 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.
  5. Not Suitable for Optional Relationships

    • If the relationship is optional, @MapsId may not be ideal.
    • Sometimes a User might exist without a UserProfile.
  6. 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.

  7. 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.

When @MapsId Is Ideal

Use it when:
the child entity is just an extension of the parent entity and shares the same lifecycle.

  • The relationship is strictly one-to-one
  • The child cannot exist without the parent
  • The child is more like an extension of the parent

Top comments (0)