One-to-One Mapping in JPA – A Complete Guide with Examples

Illustration for One-to-One Mapping in JPA – A Complete Guide with Examples
By Last updated:

In relational databases, some entities are closely related such that each instance of one entity is associated with exactly one instance of another entity. For example, a User can have exactly one Profile, or a Passport is issued to exactly one Person. This relationship is represented in JPA with the @OneToOne annotation.

JPA’s One-to-One mapping is a powerful feature that allows developers to map such relationships in an object-oriented way, avoiding repetitive joins and boilerplate SQL. It improves data modeling, code readability, and ORM performance.

In this tutorial, we’ll cover everything about One-to-One mapping in JPA — setup, annotations, CRUD operations, querying, pitfalls, and best practices.


Core Definition of One-to-One Mapping

A one-to-one relationship means:

  • One row in table A is related to exactly one row in table B.
  • Relationship can be unidirectional (A → B) or bidirectional (A ↔ B).
  • Implemented in DB using a foreign key or a shared primary key.

Setup and Configuration

persistence.xml

<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
    <persistence-unit name="my-pu" transaction-type="RESOURCE_LOCAL">
        <class>com.example.User</class>
        <class>com.example.Profile</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Entity Mapping with @OneToOne

Example: User ↔ Profile

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "profile_id", referencedColumnName = "id")
    private Profile profile;

    // Getters and setters
}

@Entity
@Table(name = "profiles")
public class Profile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String bio;
    private String website;

    @OneToOne(mappedBy = "profile")
    private User user;

    // Getters and setters
}

Key Points

  • @OneToOne declares the relationship.
  • @JoinColumn defines the foreign key column in the owning entity.
  • mappedBy makes the relationship bidirectional.
  • cascade = CascadeType.ALL propagates operations (persist, remove, etc.).

CRUD Operations with EntityManager

Create (Insert)

EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-pu");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

tx.begin();
Profile profile = new Profile();
profile.setBio("Java Developer");
profile.setWebsite("https://prgrmmng.com");

User user = new User();
user.setUsername("ashwani");
user.setProfile(profile);

em.persist(user);
tx.commit();

Generated SQL:

insert into profiles (bio, website) values ('Java Developer', 'https://prgrmmng.com');
insert into users (username, profile_id) values ('ashwani', 1);

Read (Fetch)

User found = em.find(User.class, 1L);
System.out.println(found.getUsername() + " -> " + found.getProfile().getBio());

Update

tx.begin();
found.getProfile().setBio("Senior Java Developer");
em.merge(found);
tx.commit();

SQL:

update profiles set bio='Senior Java Developer' where id=?;

Delete

tx.begin();
em.remove(found);
tx.commit();

SQL:

delete from users where id=?;
delete from profiles where id=?;

Querying with JPA

JPQL

List<User> users = em.createQuery("SELECT u FROM User u JOIN FETCH u.profile", User.class).getResultList();

Criteria API

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
root.fetch("profile", JoinType.INNER);
List<User> results = em.createQuery(cq).getResultList();

Native SQL

List<Object[]> results = em.createNativeQuery("SELECT u.username, p.bio FROM users u JOIN profiles p ON u.profile_id = p.id").getResultList();

Persistence Context and Fetching

  • Persistence Context ensures one instance per entity per transaction.
  • Lazy vs Eager fetching:
    • Lazy = loads profile only when accessed (like ordering food when hungry).
    • Eager = loads profile immediately (like ordering all food at once).
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "profile_id")
private Profile profile;

Real-World Use Cases

  • User ↔ Profile in authentication systems.
  • Employee ↔ Address in HR applications.
  • Order ↔ Invoice in e-commerce.

Anti-Patterns and Pitfalls

  • Avoid bidirectional + eager fetching → performance bottlenecks.
  • Be cautious with CascadeType.ALL on delete → may remove unintended entities.
  • Handle nullable relationships properly with optional=false if required.

Best Practices

  • Prefer unidirectional mapping unless bidirectional is necessary.
  • Use Lazy fetching by default.
  • Tune cascades carefully (e.g., CascadeType.PERSIST instead of ALL).
  • Always test SQL queries generated.

📌 JPA Version Notes

  • JPA 2.0: Introduced Criteria API, Metamodel.
  • JPA 2.1: Added entity graphs, stored procedures.
  • Jakarta Persistence: Package migrated from javax.persistencejakarta.persistence.

Conclusion and Key Takeaways

  • @OneToOne maps unique one-to-one relationships in databases.
  • Use @JoinColumn for foreign keys and mappedBy for bidirectional mapping.
  • Choose lazy fetching and explicit cascades for performance.
  • One-to-one mapping simplifies modeling of tightly coupled entities.

By mastering this mapping, you’ll design cleaner and more efficient ORM-based applications.


FAQ

1. What’s the difference between JPA and Hibernate?
JPA is a specification, Hibernate is an implementation.

2. How does JPA handle the persistence context?
It manages entity state across transactions, ensuring identity consistency.

3. What are the drawbacks of eager fetching in JPA?
Loads unnecessary data, slowing performance.

4. How can I solve the N+1 select problem with JPA?
Use JOIN FETCH, batch fetching, or entity graphs.

5. Can I use JPA without Hibernate?
Yes, EclipseLink and OpenJPA are alternatives.

6. What’s the best strategy for inheritance mapping in JPA?
JOINED for normalization, SINGLE_TABLE for performance.

7. How does JPA handle composite keys?
With @IdClass or @EmbeddedId.

8. What changes with Jakarta Persistence?
Namespace moved to jakarta.persistence.

9. Is JPA suitable for microservices?
Yes, but consider DTOs to reduce coupling.

10. When should I avoid using JPA?
In heavy batch-processing or NoSQL-first architectures.