This article is part of the JPA Pitfalls series, and our first topic of discussion is some common JPA relationship mapping pitfalls. JPA makes it really easy to do things the wrong way, and makes it really unintuitive to do things right. In this blog post series, I will go over some common JPA pitfalls and show how to avoid them.
One-to-many relationship mapping
Assuming we have the following database schema:
We could create the following JPA entities:
- Post:
@Entity
class Post {
@Id
@GeneratedValue
private Long id;
private String content;
… // no-args constructor, getters and setters omitted for brevity
}
- Comment:
@Entity
class Comment {
@Id
@GeneratedValue
private Long id;
private String content;
@ManyToOne
private Post post;
… // no-args constructor, getters and setters omitted for brevity
}
Comment entity has a reference to a Post via a @ManyToOne binding.
Now, it might be tempting to also create a bidirectional binding, so that we could have a list of comments when we fetch a post:
@Entity
class Post {
…
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Comment> comments = Set.of();
}
If we fetch a post, we will also get all of the comments as well -- that sounds very convenient. But the problem is that we will get all the comments, and there is no way to make it pageable. Imagine if we had thousands or millions of comments on a post. We would have to fetch them all from our database!
Instead, we could just have a unidirectional @ManyToOne association on the Comment entity, and then we could run a custom query to get all post comments:
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
Set<Comment> findByPost_id(Long postId);
}
We could then make this query pageable simply by passing a Pageable instance into our repository method:
Page<Comment> findByPost_id(Long postId, Pageable pageable);
Many-to-many relationship mapping
Assuming we have the following database schema:
We could map it to the following JPA entities:
- User:
@Entity
class User {
@Id
@GeneratedValue
private Long id;
private String name;
… // no-args constructor, getters and setters omitted for brevity
}
- Movie:
@Entity
class Movie {
@Id
@GeneratedValue
private Long id;
private String title;
… // no-args constructor, getters and setters omitted for brevity
}
We could then use @ManyToMany annotation to connect these two together:
- User:
@Entity
class User {
…
@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(
name = "review",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "movie_id")
)
private Set<Movie> movieReviews;
}
- Movie:
@Entity
class Movie {
…
@ManyToMany(mappedBy = "movieReviews")
private Set<User> userReviews;
}
Unfortunately, we now have the exact same problem that we had with @OneToMany annotation. If we fetch a user, we will get all their movie reviews and vice versa. There is no way to make these queries pageable.
What we could do instead is create a new entity class that represents the join table:
class Review {
@EmbeddedId
private Id id;
@ManyToOne
@MapsId("userId")
private User user;
@ManyToOne
@MapsId("movieId")
private Movie movie;
… // no-args constructor, getters and setters omitted for brevity
@Embeddable
static class Id implements Serializable {
private Long userId;
private Long movieId;
… // no-args constructor, getters, setters, equals, and hashCode omitted for brevity
}
}
We could then run custom queries to fetch what we need:
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
Set<Review> findByUser_id(Long userId);
Set<Review> findByMovie_id(Long movieId);
}
And of course, we could pass in a Pageable instance to get a pageable response:
Page<Review> findByUser_id(Long userId, Pageable pageable);
Page<Review> findByMovie_id(Long movieId, Pageable pageable);
Conclusion
Since it is not possible to create pageable queries using @OneToMany and @ManyToMany annotations, they should usually be avoided. The only exception to this rule is if you know that there will only exist very few associated entities. For example, if we have a User entity and users can have one or more roles assigned to them, then it may be okay to use @OneToMany annotation since we know there aren’t going to be that many user roles.
It would've been better if @OneToMany and @ManyToMany were named @OneToFew and @FewToFew instead to indicate better that they should only be used when there are only a few associated entities. Alternatively, these annotations could just be deprecated or completely removed. The added convenience of these annotations is really minor compared to all the troubles they cause so we would be better without them. Unfortunately, none of this will happen as it is too late now. We can only try and educate people to stop using these annotations everywhere without thinking about performance implications.
This article is part of the JPA Pitfalls series:
- JPA Pitfalls: Eager/Lazy Fetching
- JPA Pitfalls: Generating IDs
About the author
Bojan Stipic is a Software Engineer with over three years of experience working at our engineering hub in Novi Sad.
Bojan is interested in Full-stack web development, systems programming,
programming language design, and compiler development. As for specific technologies, he feels most comfortable using technologies such as Java and React.