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

Signature
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id")
  • Added in Spring Boot JPA 1.0 (javax.persistence)

Parameters

ParameterTypeDescription
$fetchoptionalFetchType= FetchType.EAGEREAGER loads the target with the owner; LAZY loads it on first access through a proxy.
$optionaloptionalboolean= trueWhether the association may be null. false lets Hibernate use inner joins and reject nulls.
$cascadeoptionalCascadeType[]= {}Operations to cascade to the target. Rarely right on this side: the parent usually outlives the child.
$targetEntityoptionalClass<?>= the field typeThe 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.ALL on 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 @OneToMany into endless recursion. Return DTOs.
  • Forgetting optional = false when the column is NOT 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

Example · java
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;

@Entity
class Customer {
    @Id @GeneratedValue
    private Long id;
    private String name;

    protected Customer() { }

    String getName() { return name; }
}

@Entity
@Table(name = "orders")
class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(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
    @Query("select o from Order o join fetch o.customer where o.status = :status")
    List<Order> findWithCustomerByStatus(@Param("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.

Example · java
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.

Example · java
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.

Example · sql
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

  • Be the first to comment on this lesson.