JPA Pitfalls: Relationship Mapping

JPA Pitfalls: Relationship Mapping

Symphony Logo
Symphony
September 5th, 2022

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:

one to many relationship

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:

many to many relationship

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.

Contact us if you have any questions about our company or products.

We will try to provide an answer within a few days.