Bidirectional One-to-Many Relationships
Prerequisites: You should have completed Exercise 02 (Many-to-One Relationships) with Customer and Order entities.
Goal: Learn how to create bidirectional relationships, understand the owning vs inverse side, and handle JSON serialization issues.
What You'll Build
Add a List<Order> to the Customer entity so you can navigate from Customer to Orders (and vice versa).
Understanding Bidirectional Relationships
Current situation (Unidirectional):
Order→Customer: You can navigate from an order to its customerCustomer→Orders: You CANNOT navigate from a customer to their orders
After this exercise (Bidirectional):
Order→Customer:Customer→Orders:
Important concepts:
- Owning side: The side with the foreign key (
Orderhascustomer_id) - Inverse side: The side with
mappedBy(CustomerwithList<Order>) - Only the owning side matters for database updates
Step 1: Add Orders List to Customer
Update your Customer class in the model package:
package com.example.demo.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer {
// ... existing fields ...
@OneToMany(mappedBy = "customer")
private List<Order> orders = new ArrayList<>();
// ... existing constructors, getters, setters ...
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
}
Key points:
@OneToMany(mappedBy = "customer"): Creates the One-to-Many relationshipmappedBy = "customer": Refers to thecustomerfield in theOrderclass- This tells JPA: "Don't create a new foreign key; use the one that already exists in the Order table"
- Initialize the list to avoid
NullPointerException
Step 2: Test the Bidirectional Relationship
-
Restart your application
-
Check the database in H2 Console:
- No new tables or columns should be created
- The
ORDERStable still only has aCUSTOMER_IDcolumn
-
Test the customer endpoint:
GET http://localhost:8080/api/customers/1
Expected behavior: You should see an error or empty response! Let's understand why.
Step 3: Understanding the Problem - Infinite Recursion
When you try to access /api/customers/1, Jackson (the JSON serializer) tries to:
- Serialize
Customer→ includesorderslist - Serialize each
Order→ includescustomer - Serialize
Customeragain → includesorderslist - Serialize each
Orderagain → includescustomer - Infinite loop!
Step 4: Fix with Jackson Annotations
Update both Customer and Order classes to handle JSON serialization:
Customer class:
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String phone;
@OneToMany(mappedBy = "customer")
@JsonManagedReference
private List<Order> orders = new ArrayList<>();
// ... rest of the class (constructors, getters, setters)
}
Order class:
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private LocalDate orderDate;
private Double totalAmount;
@ManyToOne
@JsonBackReference
private Customer customer;
// ... rest of the class (constructors, getters, setters)
}
What do these annotations do?
@JsonManagedReference: On the "parent" side (Customer) - includes this in JSON@JsonBackReference: On the "child" side (Order) - excludes this from JSON to break the cycle
Step 5: Test Again
-
Restart your application
-
Test:
GET http://localhost:8080/api/customers/1
Expected JSON:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"phone": "12345678",
"orders": [
{
"id": 1,
"orderNumber": "ORD-001",
"orderDate": "2025-01-15",
"totalAmount": 299.99
},
{
"id": 2,
"orderNumber": "ORD-002",
"orderDate": "2025-01-16",
"totalAmount": 149.50
},
{
"id": 5,
"orderNumber": "ORD-005",
"orderDate": "2025-01-19",
"totalAmount": 1299.99
}
]
}
- Test:
GET http://localhost:8080/api/orders/1
Notice: The order no longer includes the customer object (because of @JsonBackReference)
{
"id": 1,
"orderNumber": "ORD-001",
"orderDate": "2025-01-15",
"totalAmount": 299.99
}
Step 6: Add Helper Methods (Best Practice)
To maintain both sides of the relationship, add helper methods to Customer:
package com.example.demo.model;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String phone;
@OneToMany(mappedBy = "customer")
@JsonManagedReference
private List<Order> orders = new ArrayList<>();
// Constructors...
// Helper method to add an order
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}
// Helper method to remove an order
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null);
}
// Getters and setters...
}
Why use helper methods?
- Ensures both sides of the relationship are always in sync
- Prevents bugs where you update one side but forget the other
Update your InitData to use the helper method:
@Override
public void run(String... args) throws Exception {
// Create and save customers
// Create orders and add them to customers using helper method
// Use helper method to maintain both sides
System.out.println("Initial data created:");
System.out.println("- Customers: " + customerRepository.count());
System.out.println("- Orders: " + orderRepository.count());
}
Step 7: Understanding Owning vs Inverse Side
Owning side (Order):
- Has the foreign key column (
customer_id) - Changes here affect the database
- Setting
order.setCustomer(customer)updatescustomer_idin the database
Inverse side (Customer):
- Uses
mappedBy - For convenience only (navigation in Java)
- Adding orders to
customer.getOrders()does NOT update the database! - You must set the owning side:
order.setCustomer(customer)
Test this:
// This WORKS (owning side)
order.setCustomer(customer);
orderRepository.save(order); // customer_id is saved
// This DOES NOT WORK (inverse side only)
customer.getOrders().add(order);
customerRepository.save(customer); // customer_id is NOT saved
// This WORKS (both sides maintained)
customer.addOrder(order); // Uses helper method that updates both sides
orderRepository.save(order); // customer_id is saved