One-to-Many and Many-to-One Mapping in JPA

Illustration for One-to-Many and Many-to-One Mapping in JPA
By Last updated:

In relational databases, entities are often related in such a way that one entity instance relates to multiple instances of another entity. For example:

  • A Department has many Employees.
  • An Order has many OrderItems.
  • A Blog has many Posts.

This relationship is modeled in JPA using @OneToMany and @ManyToOne. These mappings allow developers to model parent-child associations naturally in Java objects while JPA handles the underlying SQL joins.

In this tutorial, we’ll explore One-to-Many and Many-to-One mappings in JPA, covering setup, annotations, CRUD operations, performance pitfalls, and best practices.


Core Definition of One-to-Many and Many-to-One

  • One-to-Many: One entity is related to multiple entities. (e.g., one department → many employees).
  • Many-to-One: Many entities are related to one entity. (e.g., many employees → one department).
  • Typically, One-to-Many is the inverse side of a Many-to-One relationship.

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.Department</class>
        <class>com.example.Employee</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 Example

Department ↔ Employee

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "departments")
public class Department {

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

    private String name;

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Employee> employees;

    // Getters and setters
}

@Entity
@Table(name = "employees")
public class Employee {

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

    private String name;

    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;

    // Getters and setters
}

Key Points

  • @OneToMany(mappedBy = "department") makes Department the inverse side.
  • @ManyToOne with @JoinColumn defines the owning side (Employee).
  • cascade = CascadeType.ALL ensures operations propagate.
  • orphanRemoval = true deletes employees removed from the list.

CRUD Operations with EntityManager

Create (Insert)

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

tx.begin();
Department dept = new Department();
dept.setName("IT");

Employee e1 = new Employee();
e1.setName("Alice");
e1.setDepartment(dept);

Employee e2 = new Employee();
e2.setName("Bob");
e2.setDepartment(dept);

dept.setEmployees(List.of(e1, e2));
em.persist(dept);
tx.commit();

SQL Generated:

insert into departments (name) values ('IT');
insert into employees (name, department_id) values ('Alice', 1);
insert into employees (name, department_id) values ('Bob', 1);

Read

Department found = em.find(Department.class, 1L);
System.out.println(found.getName() + " -> " + found.getEmployees().size() + " employees");

Update

tx.begin();
found.setName("Information Technology");
em.merge(found);
tx.commit();

Delete

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

SQL:

delete from employees where department_id=?;
delete from departments where id=?;

Querying with JPA

JPQL

List<Employee> employees = em.createQuery("SELECT e FROM Employee e WHERE e.department.name = :dept", Employee.class)
                             .setParameter("dept", "IT")
                             .getResultList();

Criteria API

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
Root<Employee> root = cq.from(Employee.class);
cq.select(root).where(cb.equal(root.get("department").get("name"), "IT"));
List<Employee> results = em.createQuery(cq).getResultList();

Native SQL

List<Object[]> results = em.createNativeQuery("SELECT d.name, e.name FROM departments d JOIN employees e ON d.id = e.department_id").getResultList();

Persistence Context and Fetching

  • Many-to-One is EAGER by default, One-to-Many is LAZY.
  • Lazy loading = like ordering food only when needed.
  • Eager loading = like ordering everything upfront, even if unused.
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;

Real-World Use Cases

  • Department ↔ Employees in HR applications.
  • Order ↔ Items in e-commerce.
  • Blog ↔ Posts in content management systems.

Anti-Patterns and Pitfalls

  • N+1 Problem: Accessing lazy collections in loops leads to extra queries.
  • Unidirectional One-to-Many with @JoinColumn creates inefficient schemas.
  • CascadeType.ALL on Many-to-One may cause accidental deletes.

Best Practices

  • Prefer bidirectional mapping for clarity.
  • Use LAZY fetching by default.
  • Initialize collections (new ArrayList<>) to avoid NullPointerException.
  • Use JOIN FETCH in JPQL to prevent N+1 issues.

📌 JPA Version Notes

  • JPA 2.0: Introduced Criteria API, metamodel.
  • JPA 2.1: Added Entity graphs, stored procedures.
  • Jakarta Persistence: Package moved from javax.persistencejakarta.persistence.

Conclusion and Key Takeaways

  • @OneToMany and @ManyToOne model parent-child relationships in JPA.
  • Always identify the owning side (@ManyToOne).
  • Use Lazy fetching to optimize performance.
  • Watch out for N+1 queries and overuse of cascades.

By mastering these mappings, you can design scalable and efficient ORM relationships.


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 tracks entity states to reduce redundant queries.

3. What are the drawbacks of eager fetching in JPA?
Loads unnecessary data, increasing memory and query times.

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

5. Can I use JPA without Hibernate?
Yes, with EclipseLink or OpenJPA.

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?
The package was renamed to jakarta.persistence.

9. Is JPA suitable for microservices?
Yes, but DTOs and projections are recommended.

10. When should I avoid using JPA?
For massive batch processing or NoSQL-heavy systems.