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.