Handle Many-To-Many relationship with JPA in a Spring Boot application - part 2
Photo by Everton Vila / Unsplash
In the first part, we saw how to handle a Many-to-Many relationship between two JPA entities without additional. Still, sometimes you might want to store additional information in the association table.
In this part, we will see how to handle a Many-to-Many relationship that has additional information. We will continue to use our movie web application and improve the requirement.
The use case
We allowed a user to add movies he likes, and now we want to make it possible to rate the movie, add a review, and finally, the date when the movie has been added. The entity-relation diagram now looks like this:
Prerequisites
As in the previous part, you will need a MySQL instance installed on your local computer, or Docker must be installed to create a container from the MySQL Docker image. Here is the command to start a container:
docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=movied --name mysqldb -p 3307:3306 mysql:8.0
You will also need the tools required to code in Java:
Prepare the Spring Boot project
We will clone the project from part one and then continue to work with:
git clone https://github.com/tericcabrel/blog-tutorials.git
cd blog-tutorials/springboot-many-to-many-1
docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=movied --name mysqldb -p 3307:3306 mysql:8.0
mvn install
mvn spring-boot:run
You will get the output below:
Delete the simple Many-to-Many relationship
With JPA, handling Many-to-Many relationship data that hold additional information is different from the one that doesn't have; before we see how to do it, let's delete the unnecessary code.
In the file User.java of the package entities
, remove the code below:
@ManyToMany()
@JoinTable(
name = "users_movies",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "movie_id")
)
private Set<Movie> movies;
public Set<Movie> getMovies() {
return movies;
}
public void setMovies(Set<Movie> movies) {
this.movies = movies;
}
In the file Movie.java of the package entities
, remove the code below:
@ManyToMany(mappedBy = "movies")
private Set<User> users;
public Set<User> getUsers() {
return users;
}
Delete all the tables in your database to start with a consistent state.
Create the Many-to-Many relationship
With JPA, when your Many-to-Many relationship between two entities with additional information, we need to create a new Java class, let's say UserMovie.java.
But we have a problem: the primary key is the combination of user_id
and movie_id
to represent it, we create a Subclass called UserMovieId.java that has the properties userId
and movieId
. This class will have the annotation @Embeddable
Here is the content of the file UserMovie.java
:
package com.tericcabrel.movie.entities;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.MapsId;
import javax.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
@Entity
@Table(name = "users_movies")
public class UserMovie {
@EmbeddedId
private UserMovieId id = new UserMovieId();
@ManyToOne
@MapsId("userId")
private User user;
@ManyToOne
@MapsId("movieId")
private Movie movie;
@Column(nullable = false)
private int rate;
@Lob
private String review;
@CreationTimestamp
@Column(name = "added_at", nullable = false)
private Date addedAt;
public UserMovie() {}
public UserMovie(UserMovieId id, int rate, String review) {
this.id = id;
this.rate = rate;
this.review = review;
}
public UserMovieId getId() {
return id;
}
public User getUser() {
return user;
}
public Movie getMovie() {
return movie;
}
public int getRate() {
return rate;
}
public String getReview() {
return review;
}
public Date getAddedAt() {
return addedAt;
}
@Embeddable
public static class UserMovieId implements Serializable {
private static final long serialVersionUID = 1L;
private Integer userId;
private Integer movieId;
public UserMovieId() {}
public UserMovieId(Integer userId, Integer movieId) {
super();
this.userId = userId;
this.movieId = movieId;
}
public Integer getUserId() {
return userId;
}
public Integer getMovieId() {
return movieId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public void setMovieId(Integer movieId) {
this.movieId = movieId;
}
}
}
Run the application with mvn spring-boot:run
so Hibernate can create the database tables, and let's verify the result:
Create the UserMovie repository
Since the table users_movies
is now represented by a JPA entity; we need to create his own repository interface. Inside the package repositories, create a file UserMovieRepository.java and add the code below:
package com.tericcabrel.movie.repositories;
import com.tericcabrel.movie.entities.UserMovie;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMovieRepository extends CrudRepository<UserMovie, UserMovie.UserMovieId> {
}
Insert data inside the users_movies table
Let's update the content of the file DataSeeder.java
with this one:
package com.tericcabrel.movie;
import com.tericcabrel.movie.entities.Movie;
import com.tericcabrel.movie.entities.User;
import com.tericcabrel.movie.entities.UserMovie;
import com.tericcabrel.movie.entities.UserMovie.UserMovieId;
import com.tericcabrel.movie.repositories.MovieRepository;
import com.tericcabrel.movie.repositories.UserMovieRepository;
import com.tericcabrel.movie.repositories.UserRepository;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
public class DataSeeder implements ApplicationListener<ContextRefreshedEvent> {
private final UserRepository userRepository;
private final MovieRepository movieRepository;
private final UserMovieRepository userMovieRepository;
public DataSeeder(UserRepository userRepository, MovieRepository movieRepository, UserMovieRepository userMovieRepository) {
this.userRepository = userRepository;
this.movieRepository = movieRepository;
this.userMovieRepository = userMovieRepository;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
Movie movie1 = new Movie("Movie 1", "Movie 1 description", 2020);
Movie movie2 = new Movie("Movie 2", "Movie 2 description", 2021);
Movie createdMovie1 = movieRepository.save(movie1);
Movie createdMovie2 = movieRepository.save(movie2);
User user = new User("user@email.com", "John Doe");
User createdUser = userRepository.save(user);
UserMovie.UserMovieId userMovieId1 = new UserMovieId(createdUser.getId(), createdMovie1.getId());
UserMovie userMovie1 = new UserMovie(userMovieId1, 4, "This is a good movie");
userMovie1.setUser(createdUser);
userMovie1.setMovie(createdMovie1);
UserMovie.UserMovieId userMovieId2 = new UserMovieId(createdUser.getId(), createdMovie2.getId());
UserMovie userMovie2 = new UserMovie(userMovieId2, 5, "This is an awesome movie!");
userMovie2.setUser(createdUser);
userMovie2.setMovie(createdMovie2);
userMovieRepository.save(userMovie1);
userMovieRepository.save(userMovie2);
Iterable<UserMovie> userMovieList = userMovieRepository.findAll();
userMovieList.forEach(um -> {
System.out.println("The user " + um.getUser().getName() + " gave a rate of " + um.getRate() + " to the movie " + um.getMovie().getName());
});
}
}
Rerun the application, and you will get the output below:
That's it. You can now use Many-to-Many relationships in your Spring Boot project.
You can find the code source on the GitHub repository.
Follow me on Twitter or subscribe to my newsletter to not miss the upcoming posts and the tips and tricks I share every week.
Happy to see you soon ?