Handle Many-To-Many relationship with JPA in a Spring Boot application - part 2

Handle Many-To-Many relationship with JPA in a Spring Boot application - part 2
Photo by Everton Vila / Unsplash

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.

Handle Many-to-Many relationship with JPA in a Spring Boot application - part 1
In this post, we will see how to create many-to-many relationships between entities using JPA and Hibernate ins a Spring Boot project.

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:

The updated database schema.
The updated database schema.

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:

Run the project locally

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:

View the database schema from a GUI tool.
View the database schema from a GUI tool.

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:

The data have been inserted successfully
The data have been inserted successfully ?

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 ?