@ManyToOne
Maps a foreign key to a reference to another entity, such as an order's customer; it loads eagerly by default, so set fetch to LAZY.
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id")- Added in Spring Boot JPA 1.0 (javax.persistence)
Parameters
| Parameter | Type | Description |
|---|---|---|
$fetchoptional | FetchType= FetchType.EAGER | EAGER loads the target with the owner; LAZY loads it on first access through a proxy. |
$optionaloptional | boolean= true | Whether the association may be null. false lets Hibernate use inner joins and reject nulls. |
$cascadeoptional | CascadeType[]= {} | Operations to cascade to the target. Rarely right on this side: the parent usually outlives the child. |
$targetEntityoptional | Class<?>= the field type | The target entity type when it cannot be inferred from the field type. |
Return value
n/aMaps a single-valued association whose foreign key column lives in this entity's table.
Errors
Accessing a lazy association after the persistence context has closed throws LazyInitializationException.
@ManyToOne is the most common association and the simplest: many orders belong to one customer, so the orders table has a customer_id column. In Java you hold a Customer reference instead of a raw id, and Hibernate writes the foreign key for you. This side owns the relationship; whatever you set here is what reaches the database.
Always make it lazy
The JPA default for @ManyToOne is FetchType.EAGER. Every time you load an order, Hibernate loads its customer too, and when you run a JPQL query for 100 orders it issues up to 100 extra SELECTs for the customers: the N+1 problem. Eager loading cannot be switched off per query, while a lazy association can be fetched per query with join fetch or an entity graph. So the rule is simple: write fetch = FetchType.LAZY on every @ManyToOne.
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;A lazy reference is a proxy. Its id is available without a query, but reading any other field needs an open persistence context. With spring.jpa.open-in-view=false (recommended), touch it inside the transactional service method or fetch it in the query, not in the controller.
Setting the reference without loading the parent
To create an order for customer 42 you do not need the customer's data, only its key. repository.getReferenceById(42L) returns a proxy without running a query, and saving the order writes customer_id = 42. If the customer does not exist, the foreign key constraint rejects the insert. Use findById when you need a clean 404 instead.
Mistakes to avoid
cascade = CascadeType.ALLon a@ManyToOne: deleting one order then tries to delete the customer as well, which fails on the foreign keys of the customer's other orders or, if those cascade too, removes data you meant to keep.- Serialising the entity to JSON: Jackson touches the proxy and either fails with a lazy-loading error or walks back through a
@OneToManyinto endless recursion. Return DTOs. - Forgetting
optional = falsewhen the column isNOT NULL. It documents the rule and lets Hibernate use inner joins.
Start with a unidirectional @ManyToOne. Add the inverse @OneToMany only when you genuinely navigate from parent to children.
Example
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
class Customer {
private Long id;
private String name;
protected Customer() { }
String getName() { return name; }
}
(name = "orders")
class Order {
private Long id;
(fetch = FetchType.LAZY, optional = false)
(name = "customer_id", nullable = false)
private Customer customer;
private String status;
protected Order() { }
Order(Customer customer) {
this.customer = customer;
this.status = "NEW";
}
Customer getCustomer() { return customer; }
}
public interface OrderRepository extends JpaRepository<Order, Long> {
// one query with a join instead of 1 + N
("select o from Order o join fetch o.customer where o.status = :status")
List<Order> findWithCustomerByStatus(("status") String status);
}When to use it
- Linking each order to its customer while listing orders with a single join-fetch query.
- Adding a comment to a post given only the post id, without loading the post.
- Assigning tasks to projects where every task must have a project (optional = false).
Frequently asked questions
Why is @ManyToOne eager by default?
The JPA specification chose EAGER for single-valued associations. Hibernate's own guidance is to make every association lazy and fetch what each use case needs with join fetch or entity graphs.
How do I get the foreign key id without loading the parent?
Call getId() on the lazy reference. Hibernate's proxy holds the id, so reading it does not run a query, as long as the id getter is a plain accessor.
Do I need @JoinColumn?
No. Without it the column name defaults to the field name, an underscore and the target's primary key column, for example customer_id. Add it to name the column explicitly or to declare nullable = false for generated schemas.
More examples
Creating a child with getReferenceById
Only an INSERT into task runs. If projectId does not exist, the foreign key constraint fails the insert, which surfaces as a DataIntegrityViolationException; map it to a 404 or 422 in your error handler.
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Entity
class Project {
@Id @GeneratedValue
private Long id;
protected Project() { }
}
@Entity
class Task {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Project project;
private String title;
protected Task() { }
Task(Project project, String title) { this.project = project; this.title = title; }
Long getId() { return id; }
}
interface ProjectRepository extends JpaRepository<Project, Long> { }
interface TaskRepository extends JpaRepository<Task, Long> { }
@Service
public class TaskService {
private final ProjectRepository projects;
private final TaskRepository tasks;
public TaskService(ProjectRepository projects, TaskRepository tasks) {
this.projects = projects;
this.tasks = tasks;
}
@Transactional
public Long addTask(Long projectId, String title) {
Project ref = projects.getReferenceById(projectId); // no SELECT
return tasks.save(new Task(ref, title)).getId();
}
}Reading the parent inside the transaction
The DTO is built inside the read-only transaction, so the lazy author loads safely. Returning the Post itself and reading the author in the controller would fail once open-in-view is turned off.
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Entity
class Author {
@Id @GeneratedValue
private Long id;
private String name;
protected Author() { }
String getName() { return name; }
}
@Entity
class Post {
@Id @GeneratedValue
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Author author;
protected Post() { }
String getTitle() { return title; }
Author getAuthor() { return author; }
}
interface PostRepository extends JpaRepository<Post, Long> { }
record PostView(String title, String authorName) { }
@Service
public class PostQueries {
private final PostRepository posts;
public PostQueries(PostRepository posts) { this.posts = posts; }
@Transactional(readOnly = true)
public PostView find(Long id) {
Post post = posts.findById(id).orElseThrow();
// the proxy is initialised here, while the session is still open
return new PostView(post.getTitle(), post.getAuthor().getName());
}
}The schema behind the mapping
PostgreSQL does not index foreign key columns automatically. Without the index, loading a customer's orders or deleting a customer scans the whole orders table.
create table orders (
id bigint primary key,
customer_id bigint not null references customer (id),
status varchar(20) not null
);
create index orders_customer_id_idx on orders (customer_id);
Discussion