Many-to-One Mapping
Beginner@ManyToOne is the owning side and physically holds the FK column; it is the simplest and most common relationship — e.g., many Orders to one Customer.
Overview
@ManyToOne is the most common JPA relationship. Many orders belong to one customer — the order table holds a customer_id FK. @ManyToOne is always the owning side and always holds the FK column (@JoinColumn). The inverse side (@OneToMany with mappedBy) is optional and purely navigational — adding it does not change the DB schema. Always declare @ManyToOne with FetchType.LAZY (the default for @OneToMany is LAZY, but for @ManyToOne it is EAGER by default in JPA — change it explicitly).
Basic @ManyToOne
@ManyToOne on the child entity creates a FK column pointing to the parent PK. @JoinColumn names the FK column. FetchType defaults to EAGER for @ManyToOne — always override to LAZY.
// Order (child) → Customer (parent): many orders, one customer
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// FetchType.EAGER is the JPA default for @ManyToOne — always override!
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
private OrderStatus status;
private BigDecimal total;
}
// Customer (parent) — owns no FK; inverse side is optional
@Entity
@Table(name = "customers")
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
// Optional inverse navigation — does NOT change DB schema
@OneToMany(mappedBy = "customer", // "customer" = field name in Order
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
}Bidirectional Sync Helper
In a bidirectional relationship, both sides must be kept in sync when adding or removing children. Forgetting to set the back-reference causes the L1 cache to return stale collections.
// Bidirectional sync helpers on the parent
@Entity
public class Customer {
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// Sync helper — sets both sides
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this); // set back-reference on child
}
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null); // clear back-reference
}
}
// Usage
Customer customer = customerRepository.findById(42L).orElseThrow();
Order newOrder = new Order(OrderStatus.DRAFT, BigDecimal.ZERO);
customer.addOrder(newOrder); // both sides synced
customerRepository.save(customer); // cascades to order
// ✗ Wrong — only sets child side, parent collection is stale in L1 cache
// newOrder.setCustomer(customer);
// orderRepository.save(newOrder);N+1 Problem with @ManyToOne
Loading a list of orders and then accessing order.getCustomer() for each one causes N+1 SELECT queries. Fix with JPQL JOIN FETCH or @EntityGraph to load the association in one query.
// N+1 problem — 1 query for orders + N queries for customers
List<Order> orders = orderRepository.findAll(); // SELECT * FROM orders
orders.forEach(o -> log.info(o.getCustomer().getEmail())); // N SELECT from customers
// Fix 1 — JPQL JOIN FETCH
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.customer WHERE o.status = :status")
List<Order> findWithCustomerByStatus(@Param("status") OrderStatus status);
// Single SQL: SELECT o.*, c.* FROM orders o JOIN customers c ON ...
}
// Fix 2 — @EntityGraph
@EntityGraph(attributePaths = {"customer"})
List<Order> findByStatus(OrderStatus status);
// Fix 3 — Hibernate @BatchSize (reduces N+1 to N/batchSize round trips)
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 25) // loads 25 customers per round trip
@JoinColumn(name = "customer_id")
private Customer customer;Key Points to Remember
- 1@ManyToOne is always the owning side and holds the FK column (@JoinColumn).
- 2JPA defaults @ManyToOne to FetchType.EAGER — always override to FetchType.LAZY.
- 3The inverse @OneToMany (mappedBy) is optional and does not create any DB column.
- 4Keep bidirectional relationships in sync with helper methods on the parent.
- 5N+1 queries occur when accessing a LAZY @ManyToOne in a loop — fix with JOIN FETCH or @EntityGraph.
- 6@BatchSize reduces N+1 from N round trips to N/batchSize round trips.
Interview Questions
Sign in to ask AriaWhich side of @ManyToOne owns the foreign key column?
Why is FetchType.EAGER dangerous for @ManyToOne in practice?
What is the N+1 query problem and how do you solve it for @ManyToOne?
What does mappedBy on @OneToMany mean and what does it generate in the DB?
Why do you need sync helper methods in a bidirectional relationship?
Ask Aria about Many-to-One Mapping
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.