@OneToMany and @ManyToOne JPA Mapping in Java

JPA (Java Persistence API) provides powerful annotations to define relationships between entities. The @OneToMany and @ManyToOne annotations are used to represent one-to-many and many-to-one relationships between database tables.

Understanding the Relationships

@ManyToOne

  • Represents "many instances of this entity belong to one instance of another entity"
  • The owning side of the relationship
  • Foreign key is stored in the table of the entity with @ManyToOne

@OneToMany

  • Represents "one instance of this entity has many instances of another entity"
  • The inverse side of the relationship
  • No foreign key column in the database table

Basic Implementation

Entity Classes Example

Department Entity (One-to-Many Side)

package com.example.model;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "code")
private String code;
// One-to-Many relationship with Employee
@OneToMany(mappedBy = "department", 
cascade = CascadeType.ALL, 
fetch = FetchType.LAZY)
private List<Employee> employees = new ArrayList<>();
// Constructors
public Department() {}
public Department(String name, String code) {
this.name = name;
this.code = code;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public List<Employee> getEmployees() {
return employees;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
// Helper methods for bidirectional relationship
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", name='" + name + '\'' +
", code='" + code + '\'' +
'}';
}
}

Employee Entity (Many-to-One Side)

package com.example.model;
import javax.persistence.*;
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email")
private String email;
@Column(name = "salary")
private Double salary;
// Many-to-One relationship with Department
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
// Constructors
public Employee() {}
public Employee(String firstName, String lastName, String email, Double salary) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.salary = salary;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", salary=" + salary +
'}';
}
}

Cascade Types

CascadeType defines what operations should be cascaded from the parent entity to the child entities.

// Common cascade configurations
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL) // All operations cascade
@OneToMany(mappedBy = "department", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE) // Only remove cascades

Available Cascade Types:

  • PERSIST - Cascade persist operation
  • MERGE - Cascade merge operation
  • REMOVE - Cascade remove operation
  • REFRESH - Cascade refresh operation
  • DETACH - Cascade detach operation
  • ALL - All of the above

Fetch Types

// Fetch configurations
@ManyToOne(fetch = FetchType.LAZY)  // Load on demand (recommended for @ManyToOne)
@OneToMany(fetch = FetchType.LAZY)  // Load on demand (recommended for @OneToMany)
@ManyToOne(fetch = FetchType.EAGER) // Load immediately (can cause N+1 query problem)

Repository Interfaces

package com.example.repository;
import com.example.model.Department;
import com.example.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
Department findByName(String name);
@Query("SELECT d FROM Department d LEFT JOIN FETCH d.employees WHERE d.id = :id")
Department findByIdWithEmployees(@Param("id") Long id);
}
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByDepartmentId(Long departmentId);
List<Employee> findByDepartmentName(String departmentName);
@Query("SELECT e FROM Employee e WHERE e.salary > :minSalary")
List<Employee> findEmployeesWithSalaryGreaterThan(@Param("minSalary") Double minSalary);
}

Service Layer Implementation

package com.example.service;
import com.example.model.Department;
import com.example.model.Employee;
import com.example.repository.DepartmentRepository;
import com.example.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class CompanyService {
@Autowired
private DepartmentRepository departmentRepository;
@Autowired
private EmployeeRepository employeeRepository;
// Department operations
public Department createDepartment(Department department) {
return departmentRepository.save(department);
}
public Department getDepartmentWithEmployees(Long departmentId) {
return departmentRepository.findByIdWithEmployees(departmentId);
}
// Employee operations
public Employee createEmployee(Employee employee) {
return employeeRepository.save(employee);
}
public Employee assignEmployeeToDepartment(Long employeeId, Long departmentId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new RuntimeException("Employee not found"));
Department department = departmentRepository.findById(departmentId)
.orElseThrow(() -> new RuntimeException("Department not found"));
employee.setDepartment(department);
return employeeRepository.save(employee);
}
public List<Employee> getEmployeesByDepartment(Long departmentId) {
return employeeRepository.findByDepartmentId(departmentId);
}
// Bidirectional relationship helper
public Department addEmployeeToDepartment(Long departmentId, Employee employee) {
Department department = departmentRepository.findById(departmentId)
.orElseThrow(() -> new RuntimeException("Department not found"));
department.addEmployee(employee);
return departmentRepository.save(department);
}
}

Controller Layer

package com.example.controller;
import com.example.model.Department;
import com.example.model.Employee;
import com.example.service.CompanyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class CompanyController {
@Autowired
private CompanyService companyService;
@PostMapping("/departments")
public ResponseEntity<Department> createDepartment(@RequestBody Department department) {
Department savedDepartment = companyService.createDepartment(department);
return ResponseEntity.ok(savedDepartment);
}
@PostMapping("/employees")
public ResponseEntity<Employee> createEmployee(@RequestBody Employee employee) {
Employee savedEmployee = companyService.createEmployee(employee);
return ResponseEntity.ok(savedEmployee);
}
@PutMapping("/employees/{employeeId}/department/{departmentId}")
public ResponseEntity<Employee> assignToDepartment(
@PathVariable Long employeeId, 
@PathVariable Long departmentId) {
Employee employee = companyService.assignEmployeeToDepartment(employeeId, departmentId);
return ResponseEntity.ok(employee);
}
@GetMapping("/departments/{id}/employees")
public ResponseEntity<List<Employee>> getDepartmentEmployees(@PathVariable Long id) {
List<Employee> employees = companyService.getEmployeesByDepartment(id);
return ResponseEntity.ok(employees);
}
@PostMapping("/departments/{departmentId}/employees")
public ResponseEntity<Department> addEmployeeToDepartment(
@PathVariable Long departmentId, 
@RequestBody Employee employee) {
Department department = companyService.addEmployeeToDepartment(departmentId, employee);
return ResponseEntity.ok(department);
}
}

Advanced Mapping Examples

1. Unidirectional One-to-Many (Without @ManyToOne)

@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") // Foreign key in OrderItem table
private List<OrderItem> items = new ArrayList<>();
}
@Entity
@Table(name = "order_items")
public class OrderItem {
// No reference back to Order
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private Integer quantity;
}

2. Using @JoinTable for One-to-Many

@Entity
public class University {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(
name = "university_students",
joinColumns = @JoinColumn(name = "university_id"),
inverseJoinColumns = @JoinColumn(name = "student_id")
)
private List<Student> students = new ArrayList<>();
}

3. With Additional Relationship Attributes

@Entity
public class Course {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private List<CourseRegistration> registrations = new ArrayList<>();
}
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
private List<CourseRegistration> registrations = new ArrayList<>();
}
@Entity
public class CourseRegistration {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
private Course course;
private LocalDate registrationDate;
private Integer grade;
}

Best Practices

1. Always Use Bidirectional Relationships

// Good - Bidirectional
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees;
}
public class Employee {
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}

2. Use Lazy Fetching by Default

@ManyToOne(fetch = FetchType.LAZY)  // Recommended
@OneToMany(fetch = FetchType.LAZY)  // Recommended

3. Implement Helper Methods

// In Department class
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}

4. Handle Cascading Carefully

// Be specific about which operations should cascade
@OneToMany(mappedBy = "department", 
cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Employee> employees;

Common Issues and Solutions

1. N+1 Query Problem

Problem: Lazy loading causes multiple queries
Solution: Use JOIN FETCH in queries

@Query("SELECT d FROM Department d JOIN FETCH d.employees WHERE d.id = :id")
Department findByIdWithEmployees(@Param("id") Long id);

2. Transaction Management

Problem: LazyInitializationException outside transaction
Solution: Use @Transactional or eager fetching strategically

3. Circular References in JSON

Problem: Infinite recursion when serializing to JSON
Solution: Use @JsonIgnore or DTOs

@OneToMany(mappedBy = "department")
@JsonIgnore
private List<Employee> employees;

This comprehensive guide covers the essential aspects of @OneToMany and @ManyToOne mappings in JPA, providing you with the knowledge to implement these relationships effectively in your Java applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper