DEV Community

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

Posted on • Edited on

JPA Mapping with Hibernate-One-to-One Relationship

Table of Contents

  1. What is JPA?
  2. What is Hibernate?
  3. JPA vs Hibernate — Key Differences
  4. Relationship Mapping in JPA — Quick Reference
  5. What is a One-to-One Relationship?
  6. Bidirectional One-to-One — Standard Mapping
  7. The Lazy Loading Problem on the Inverse Side
  8. Solutions to the Inverse-Side Probe Query
  9. Deep Dive — @MapsId
  10. Comparison — @JoinColumn vs @MapsId
  11. Quick Reference Table
  12. Summary

1. What is JPA?

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

2. What is Hibernate?

Hibernate is a popular open-source ORM framework that provides a concrete implementation of the JPA specification.

It allows developers to interact with databases using Java objects instead of writing complex SQL queries, making application code loosely coupled with the underlying database.

Key points:

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

3. JPA vs Hibernate — Key Differences

JPA Hibernate
Type Specification (standard API) Implementation of JPA
Who defines it Jakarta EE / Oracle Red Hat / JBoss
Can run standalone? No — needs an implementation Yes
Extra features Standard only Caching, @BatchSize, @NaturalId, etc.
Code coupling Low — portable across providers Higher — Hibernate-specific annotations

In practice:

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

4. Relationship Mapping in JPA — Quick Reference

Annotation Relationship FK Location
@OneToOne One entity ↔ one entity Child / owning side
@OneToMany One entity → many entities Child table
@ManyToOne Many entities → one entity Owning entity (FK here)
@ManyToMany Many entities ↔ many entities Join / junction table

Key best practices across all relationship types:

  • Always use FetchType.LAZY on relationships — avoids N+1 query problems
  • mappedBy goes on the non-owning side (the side without the FK column)
  • Cascade carefully — only cascade from parent to child, never upward
  • @JoinColumn explicitly names your FK column for clarity
  • Use Set instead of List for @ManyToMany to avoid duplicate join queries

5. What is a One-to-One Relationship?

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

Real-world examples:

  • PersonPassport
  • UserUserProfile
  • OrderInvoice

Unidirectional vs Bidirectional

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

User  →  UserProfile      (User knows about UserProfile; UserProfile does NOT know about User)
Enter fullscreen mode Exit fullscreen mode

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

User  ⇆  UserProfile      (both sides can navigate to the other)
Enter fullscreen mode Exit fullscreen mode

How to Identify Owner, FK Side, and Child

Ask yourself these three questions:

Question Answer
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

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

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

6. Bidirectional One-to-One — Standard Mapping

In a User ↔ UserProfile relationship, UserProfile is the child because the foreign key (user_id) lives in the user_profile table. User is the parent (inverse side).

users                  user_profile
─────────────          ──────────────────────
id  (PK)        ◀──    id  (PK)
username               phone
password               address
enabled                user_id  (FK) ← FK lives here
Enter fullscreen mode Exit fullscreen mode

The Child Entity (Owning Side)

@JoinColumn is used on the owning side — the child entity contains the foreign key pointing to the parent's primary key.

@Entity
@Table(name = "user_profile")
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)   // FK column lives here
    private User user;
}
Enter fullscreen mode Exit fullscreen mode

The Parent Entity (Inverse Side)

@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<>();

    // Bidirectional sync helper — keeps both sides consistent
    public void setProfile(UserProfile profile) {
        this.profile = profile;
        if (profile != null) {
            profile.setUser(this);
        }
    }

    // Role helper methods
    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);
    }

    @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

Note on @ElementCollection: @ElementCollection stores simple values (like enum Role) in a separate table — it is not a full entity relationship, so there is no "other side" to sync.
Keeping FetchType.EAGER here is intentional when using Spring SecurityUserDetails needs roles immediately on authentication.


7. The Lazy Loading Problem on the Inverse Side

Declaring fetch = FetchType.LAZY on the inverse side (mappedBy) of a @OneToOne does not actually make it lazy. Hibernate silently fires an extra query anyway.

Why the Extra Query Fires

On the owning side, the FK column is in the same row — Hibernate immediately knows if the association is null or not, and can safely create a proxy.

On the inverse side, there is no FK column. Hibernate must probe the database just to determine whether a UserProfile exists for a given User.

Query 1: SELECT * FROM users           your actual request
Query 2: SELECT * FROM user_profile    YOU DIDN'T ASK FOR THIS (Hibernate probe)
Query 3: SELECT * FROM user_roles     ← expected (EAGER @ElementCollection)
Enter fullscreen mode Exit fullscreen mode

Query 2 is Hibernate asking: "does a profile exist for this user?" — because the inverse side has no FK column to check locally.


8. Solutions to the Inverse-Side Probe Query

Option 1 — @LazyToOne Bytecode Instrumentation

Forces truly lazy loading via Hibernate bytecode enhancement — Hibernate injects an interceptor into the bytecode so it never needs to probe the DB.

// User.java
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY)   // forces true lazy via bytecode
private UserProfile profile;
Enter fullscreen mode Exit fullscreen mode

Requires adding the Hibernate bytecode enhancer plugin to your build tool:

<!-- Maven -->
<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    <configuration>
        <enableLazyInitialization>true</enableLazyInitialization>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode
Without Enhancement With Enhancement
Hibernate probes DB to check if profile is null Bytecode interceptor handles it — no probe needed
Extra query always fires Truly lazy — query only fires on access

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

Hibernate generates:

SELECT
    u.id, u.enabled, u.password, u.username,
    p.id, p.address, p.phone
FROM users u
LEFT JOIN user_profile p ON u.id = p.user_id
Enter fullscreen mode Exit fullscreen mode
Without JOIN FETCH With JOIN FETCH
SELECT FROM users + SELECT FROM user_profile (2 queries) SELECT FROM users LEFT JOIN user_profile (1 query)

Option 3 — @NamedEntityGraph

A @NamedEntityGraph defines a pre-declared fetch plan that can be reused across multiple repository methods, avoiding the need to write JOIN FETCH everywhere.

@NamedEntityGraph(
    name = "User.withDetails",
    attributeNodes = {
        @NamedAttributeNode("roles"),
        @NamedAttributeNode("profile")
    }
)
@Entity
@Table(name = "users")
public class User { ... }
Enter fullscreen mode Exit fullscreen mode

Use it in the repository:

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u LEFT JOIN FETCH u.profile")
    List<User> findAllWithProfile();

    @EntityGraph("User.withDetails")   // applies the named graph
    List<User> findAll();
}
Enter fullscreen mode Exit fullscreen mode

Combining with @NamedSubgraph for nested associations:

When you need to fetch deeply nested associations (e.g., User → UserProfile → Address), use @NamedSubgraph:

@Entity
@NamedEntityGraph(
    name = "User.profile",
    attributeNodes = {
        @NamedAttributeNode(value = "profile", subgraph = "profile-subgraph")
    },
    subgraphs = {
        @NamedSubgraph(
            name = "profile-subgraph",
            attributeNodes = {
                @NamedAttributeNode("address")
            }
        )
    }
)
public class User { ... }
Enter fullscreen mode Exit fullscreen mode
@Entity
public class UserProfile {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Address address;
}
Enter fullscreen mode Exit fullscreen mode
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(value = "User.profile", type = EntityGraph.EntityGraphType.FETCH)
    Optional<User> findById(Long id);
}
Enter fullscreen mode Exit fullscreen mode

With the entity graph, Hibernate generates a single query:

SELECT u.*, p.*, a.*
FROM users u
LEFT JOIN user_profile p ON p.user_id = u.id
LEFT JOIN address a       ON p.address_id = a.id
WHERE u.id = ?
Enter fullscreen mode Exit fullscreen mode

When to use @EntityGraph:

  • You want dynamic fetch strategies per query
  • You want associations to be LAZY by default
  • You want to avoid scattering JOIN FETCH across all queries
  • Different repository methods need different fetch plans

Option 4 — @MapsId Shared Primary Key (Best & Simplest)

Since both entities share the same PK, Hibernate already knows the profile ID without probing. This is the cleanest solution for a strict @OneToOne relationship.

See Section 9 — Deep Dive: @MapsId for full details.


9. Deep Dive — @MapsId

The best way to map a @OneToOne relationship in Hibernate is 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 PK as its own PK
  • This avoids creating an additional FK column
  • Hibernate already knows the child's ID — no probe query needed
users                  user_profile
─────────────          ──────────────────────
id  (PK)        ═══    id  (PK = FK) ← shared PK, no separate user_id column
username               phone
password               address
Enter fullscreen mode Exit fullscreen mode
@Entity
@Table(name = "user_profile")
public class UserProfile {

    @Id                          // No @GeneratedValue — value is copied from User
    private Long id;

    @Column(nullable = false)
    private String phone;

    @Column(nullable = false)
    private String address;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId                      // this entity's PK = users.id
    @JoinColumn(name = "id")
    private User user;
}
Enter fullscreen mode Exit fullscreen mode

The id column in user_profile 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 — no profile field needed Yes — has @MapsId
Probe query on fetch Gone — no inverse field
Save strategy userRepository.save(user) userProfileRepository.save(profile)

Advantages of @MapsId

  • No extra FK column — cleaner schema, better normalization
  • No probe query — Hibernate already knows the child's ID
  • Efficient joins — PK = FK means the join is on the same column
  • No need for bidirectional associationUser can be unidirectional
  • UserProfile can always be fetched directly using the User ID

Downsides of @MapsId

  1. Tight couplingUserProfile cannot exist without User (shares PK)
  2. Insert order dependencyUser must be persisted before UserProfile
  3. Less flexibility — migrating from one-to-one → one-to-many requires schema redesign
  4. Harder independent lifecycle management — deleting User invalidates UserProfile
  5. Not suitable for optional relationships — if a User can exist without a UserProfile, this gets awkward
  6. Harder for beginners — shared PK, @MapsId, and entity lifecycle can be confusing
  7. Schema migration complexity — adding a separate PK to the child table later requires a full refactor

When @MapsId Is Ideal

Use @MapsId when:

  • The relationship is strictly one-to-one
  • The child cannot exist without the parent
  • The child is more like an extension of the parent (e.g., UserProfile is just extra columns for User)
  • You want zero extra queries and a cleaner schema

10. Comparison — @JoinColumn vs @MapsId

@JoinColumn on child @MapsId
Profile FK column user_profile.user_id user_profile.id = users.id
Hibernate knows child ID? Must probe DB Already has it
Extra query on getUser()? Always fires (inverse side) Only when accessed
DB schema Extra user_id column Shared PK — cleaner
Optional relationship? ✅ Yes ❌ Not ideal
Child exists independently? ✅ Can ❌ Cannot
Schema simplicity Slightly more columns Minimal columns

11. Quick Reference Table

Topic Recommendation Notes
Owning side Entity holding the FK (@JoinColumn) Always the child
Inverse side Entity with mappedBy No FK — navigation only
@OneToOne fetch (owning) FetchType.LAZY FK in same row — proxy safe
@OneToOne fetch (inverse) FetchType.LAZY declared, but won't be Hibernate probes regardless
Probe query fix @MapsId or JOIN FETCH @MapsId is cleanest for strict 1:1
N+1 prevention JOIN FETCH or @EntityGraph @NamedSubgraph for nested graphs
Cascade CascadeType.ALL on parent only Never cascade from child to parent
orphanRemoval true for owned children Handles disassociation too
equals/hashCode Based on business key Never based on database ID
Collection for roles Set<Role> Safer than List with multiple joins
Best overall mapping @MapsId for strict 1:1 @JoinColumn for optional or flexible

12. Summary

  • In a @OneToOne relationship, the child holds the FK (@JoinColumn); the parent uses mappedBy.
  • Declaring FetchType.LAZY on the inverse side does not make it truly lazy — Hibernate fires a probe query anyway because there is no FK column to inspect locally.
  • Four ways to fix the probe query: bytecode instrumentation (@LazyToOne), JOIN FETCH, @NamedEntityGraph, or @MapsId.
  • @MapsId is the cleanest solution for strict one-to-one relationships — the child shares the parent's PK, eliminating the extra FK column and the probe query entirely.
  • Bidirectional = convenience (user.getProfile()) but costs a probe query. Unidirectional = no probe but you lose navigation from the User side.
  • For most applications — skip bytecode plugins, go unidirectional with @MapsId, and use JOIN FETCH when you need both entities at once.
  • Always use CascadeType.ALL on the parent side only — never cascade upward from child to parent.
  • Base equals()/hashCode() on a stable business key (e.g., username), never on the auto-generated database ID.

Top comments (0)