Home/Learn/Hibernate & JPA/Many-to-One Mapping

Many-to-One Mapping

Beginner
Relationships

@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.

Java — bidirectional @ManyToOne / @OneToMany
// 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.

Java — bidirectional sync helper methods
// 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.

Java — fixing N+1 on @ManyToOne
// 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 Aria
1

Which side of @ManyToOne owns the foreign key column?

EasyTCS
2

Why is FetchType.EAGER dangerous for @ManyToOne in practice?

MediumAmazon
3

What is the N+1 query problem and how do you solve it for @ManyToOne?

HardNetflix
4

What does mappedBy on @OneToMany mean and what does it generate in the DB?

MediumInfosys
5

Why do you need sync helper methods in a bidirectional relationship?

MediumPivotal

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.

Loading discussion…