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.