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