Spring BootManyToOne
Spring Boot

ManyToOne

@ManyToOne is the owning side of the most common JPA relationship — many child rows reference one parent row via a foreign key column. It is always placed on the entity that holds the FK column (the child). @ManyToOne is the counterpart to @OneToMany and is nearly always used in bidirectional pairs.

@ManyToOne Basics

@ManyToOne marks a field whose value is a single parent entity. The FK column lives in the child table. Hibernate generates a JOIN when navigating from child to parent. FetchType.LAZY is critical — the default EAGER fetch causes a JOIN or second SELECT for every child loaded.
Java
// ── Child entity — owns the FK column: ────────────────────────────────
@Entity
@Table(name = "comments")
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String body;

    // Owning side — comments.post_id FK column:
    @ManyToOne(fetch = FetchType.LAZY)   // ALWAYS declare LAZY
    @JoinColumn(
        name = "post_id",                // FK column name in comments table
        nullable = false                 // NOT NULL constraint
    )
    private Post post;

    // Optional: ManyToOne to another entity (author):
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id", nullable = false)
    private User author;

    protected Comment() { }

    public Comment(String body, Post post, User author) {
        this.body = body;
        this.post = post;
        this.author = author;
    }

    public Long getId() { return id; }
    public String getBody() { return body; }
    public Post getPost() { return post; }
    public User getAuthor() { return author; }
    public void setBody(String body) { this.body = body; }
}

// ── Parent entity — inverse side: ─────────────────────────────────────
@Entity
@Table(name = "posts")
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL,
               orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();

    public void addComment(Comment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    protected Post() { }
    public Post(String title) { this.title = title; }
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public List<Comment> getComments() { return Collections.unmodifiableList(comments); }
}

Fetch Strategy — Always Use LAZY

The JPA default for @ManyToOne is FetchType.EAGER — Hibernate loads the parent with every child SELECT. In a deeply associated graph this triggers a waterfall of JOINs. Always override to LAZY and use JOIN FETCH only when the parent is actually needed.
Java
// ── WRONG — default EAGER (JPA spec default for @ManyToOne): ─────────
@ManyToOne                          // FetchType.EAGER by default
@JoinColumn(name = "post_id")
private Post post;
// SELECT comment.*, post.* FROM comments JOIN posts ... (every comment load)

// ── CORRECT — always declare LAZY: ───────────────────────────────────
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
// SELECT * FROM comments  (post loaded only when comment.getPost() is called)

// ── Repository — JOIN FETCH when parent is needed: ────────────────────
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {

    // Load comments with post in one query:
    @Query("SELECT c FROM Comment c JOIN FETCH c.post WHERE c.id = :id")
    Optional<Comment> findByIdWithPost(@Param("id") Long id);

    // Load all comments for a post with author:
    @Query("SELECT c FROM Comment c " +
           "JOIN FETCH c.author " +
           "WHERE c.post.id = :postId " +
           "ORDER BY c.id ASC")
    List<Comment> findByPostIdWithAuthor(@Param("postId") Long postId);

    // Derived method — Hibernate joins post automatically for the WHERE clause:
    List<Comment> findByPostId(Long postId);     // no JOIN FETCH — parent not loaded
    List<Comment> findByAuthorId(Long authorId);
    long countByPostId(Long postId);
}

Saving and Updating ManyToOne Relationships

When saving a child with a @ManyToOne reference, Hibernate writes the parent's FK value into the child's FK column. Cascade settings on @ManyToOne are usually omitted — the parent is typically an independent entity that exists before the child is created.
Java
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {

    private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    // ── Save child with existing parent (common pattern): ─────────────
    public CommentResponse create(Long postId, CreateCommentRequest req,
                                  Long authorId) {
        // Load parent references — do not create new Post/User objects:
        Post post = postRepository.getReferenceById(postId);
        // getReferenceById() returns a proxy — no SELECT until access.
        // Use when you only need the FK, not the parent's data.

        User author = userRepository.getReferenceById(authorId);

        Comment comment = new Comment(req.body(), post, author);
        return CommentResponse.from(commentRepository.save(comment));
        // INSERT INTO comments (body, post_id, author_id) VALUES (?, ?, ?)
    }

    // ── Reassign a comment to a different post: ────────────────────────
    public void moveComment(Long commentId, Long newPostId) {
        Comment comment = commentRepository.findById(commentId).orElseThrow();
        Post newPost = postRepository.getReferenceById(newPostId);
        comment.setPost(newPost);
        // Dirty checking generates: UPDATE comments SET post_id = ? WHERE id = ?
    }

    // ── getReferenceById vs findById: ─────────────────────────────────
    // getReferenceById(id) — returns a Hibernate proxy; no SELECT.
    //   Use when you only need the FK written to the child table.
    //   Throws EntityNotFoundException on first access if ID doesn't exist.
    // findById(id)         — executes SELECT; returns populated entity.
    //   Use when you need to read the parent's data.
}

ManyToOne in Projections and DTOs

When mapping @ManyToOne fields to response DTOs, always do the mapping inside the transaction while the session is open. Never serialise a managed entity directly — Jackson will trigger lazy loads for every nested entity, producing N+1 queries.
Java
// ── DTO — flat projection of child + parent data: ────────────────────
public record CommentResponse(
    Long id,
    String body,
    Long postId,
    String postTitle,
    Long authorId,
    String authorName
) {
    public static CommentResponse from(Comment comment) {
        return new CommentResponse(
            comment.getId(),
            comment.getBody(),
            comment.getPost().getId(),        // accesses lazy association
            comment.getPost().getTitle(),     // must be inside transaction
            comment.getAuthor().getId(),
            comment.getAuthor().getName()
        );
    }
}

// ── Service — map to DTO while session is open: ────────────────────────
@Transactional(readOnly = true)
public List<CommentResponse> findByPostId(Long postId) {
    // JOIN FETCH author to prevent N+1 on author access in from():
    return commentRepository.findByPostIdWithAuthor(postId)
        .stream()
        .map(CommentResponse::from)   // session open — lazy loads succeed
        .toList();
}

// ── Interface projection — select only needed columns: ────────────────
public interface CommentSummary {
    Long getId();
    String getBody();
    Long getPostId();          // Spring Data resolves post.id as post_id
    String getPostTitle();     // Spring Data resolves post.title
    String getAuthorName();    // Spring Data resolves author.name
}

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
    List<CommentSummary> findByPostId(Long postId);
    // Spring Data generates: SELECT c.id, c.body, p.id, p.title, a.name
    //                        FROM comments c JOIN posts p JOIN users a ...
}