Handle Many-to-Many relationship with JPA in a Spring Boot application - part 1

Photo by Priscilla Du Preez / Unsplash
Photo by Priscilla Du Preez / Unsplash

Photo by Priscilla Du Preez / Unsplash

Entity Relationship is the way to link the entities in our application. These relationships are translated into database tables in an RDBMS. We usually have three types of relationships:

  • One-To-One
  • Many-To-One or One-To-Many
  • Many-To-Many

Handle Many-To-Many relationship is always tricky, especially when additional information is stored inside the relationship.

In this post, we will see how to handle a Many-to-Many relationship without additional data first and secondly with additional data. We will use Hibernate, the most popular ORM for Java that uses the JPA under the hood to create the class entity with annotation.

The use case

Let's build a movie streaming platform, and we want to develop a feature that allows the user to select his favorite movies. The entity-relationship will look like this:

Entity-relationship diagram for a favorite movies feature
Entity-relationship diagram for a favorite movies feature

Prerequisites

To follow this tutorial, 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:

Setup the Spring Boot Project

We will use the SpringBoot initializer to create the project. The only dependency we need for this project is Spring Web.

Create a Spring Boot project with the required dependencies 

Click the button "Generate" to download the project and open it in your favorite IDE.

Create the user entity

Create a package called entities then, create a file named User.java representing the user entity. Add the code below:

package com.tericcabrel.movie.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Table(name = "users")
@Entity
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(nullable = false)
  private Integer id;

  @Column(unique = true, nullable = false, length = 100)
  private String email;

  @Column(unique = true, nullable = false, length = 100)
  private String name;

  public User() {}

  public User(String email, String name) {
    this.email = email;
    this.name = name;
  }

  public Integer getId() {
    return id;
  }

  public String getEmail() {
    return email;
  }

  public String getName() {
    return name;
  }
}

Now let's create a file called Movie.java for the movie entity and add the code below:

package com.tericcabrel.movie.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;

@Table(name = "movies")
@Entity
public class Movie {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(nullable = false)
  private Integer id;

  @Column(nullable = false, length = 150)
  private String name;

  @Lob
  private String description;

  @Column(nullable = false)
  private Integer releaseDate;

  public Movie() {
  }

  public Movie(String name, String description, Integer releaseDate) {
    this.name = name;
    this.description = description;
    this.releaseDate = releaseDate;
  }

  public Integer getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public String getDescription() {
    return description;
  }

  public Integer getReleaseDate() {
    return releaseDate;
  }
}

Create the Many-to-Many relationship between User and Movie

We use the annotation @ManyToMany to define the relationship in the entity. We have two entities; where do we define the annotation?

It depends on your data access pattern: Do you want to:

  • Get movies liked by a user?

or

  • Get users who liked a movie?

We will go for the first option so let's update the file User.java to add 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;
}

If you still want to get the users who liked a movie, you can add a relationship in the Movie entity, but the declaration is straightforward. Add the code below in the file Movie.java

@ManyToMany(mappedBy = "movies")
private Set<User> users;

public Set<User> getUsers() {
  return users;
}

The property mappedBy has the value movies which represents the propertymovies in the file User.java. This is a bidirectional Many-To-Many relationship.

Create the tables in the database with Hibernate.

We defined our entity relation using JPA now; let's configure the database connection in the Spring Boot configuration properties file. Update the file application.properties with the code below:

server.port=8933

spring.datasource.url=jdbc:mysql://localhost:3307/moviedb?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=secret

## Hibernate properties
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.dialect.storage_engine=innodb

Don't forget to start the container for MySQL with the command we gave in the Prerequisites section.

Start the project with the command: mvn spring-boot:run

View the data in your database with your favorite GUI tool. Below is TablePlus.

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

We used Hibernate to create the database schema, but a better way is to use database migrations. I wrote a whole post about doing that with Spring Boot and Flyway.

Handle database migrations in a SpringBoot application with Flyway
In this tutorial, we will see how to use Flyway in a SpringBoot application to create and execute database migrations.

Insert data inside the users_movies table

We need to create User and Role repositories first. A Repository acts as a middleware between the Java class and the database schema. It provides methods to interact with the database like inserting, updating, and deleting data.

Create a package called repositories then, create a file UserRepository.java.

package com.tericcabrel.movie.repositories;

import com.tericcabrel.movie.entities.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Integer> {

}

Create the file MovieRepository.java and add the code below:

package com.tericcabrel.movie.repositories;

import com.tericcabrel.movie.entities.Movie;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MovieRepository extends CrudRepository<Movie, Integer> {

}

Now Let's load our database with some movies, users, and users_movies. Create a file called DataSeeder.java and the code below:

package com.tericcabrel.movie;

import com.tericcabrel.movie.entities.Movie;
import com.tericcabrel.movie.entities.User;
import com.tericcabrel.movie.repositories.MovieRepository;
import com.tericcabrel.movie.repositories.UserRepository;
import java.util.HashSet;
import java.util.Set;
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;

  public DataSeeder(UserRepository userRepository, MovieRepository movieRepository) {
    this.userRepository = userRepository;
    this.movieRepository = movieRepository;
  }

  @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");

    Set<Movie> movies = new HashSet<>();
    movies.add(createdMovie1);
    movies.add(createdMovie2);

    user.setMovies(movies);

    User createdUser = userRepository.save(user);

    createdUser.getMovies().forEach(System.out::println);
  }
}

This file will be executed when the server starts. Rerun the server and check your database to see the data inserted.

Data was inserted successfully in the Many-to-Many table.

Yeah ?  We are done with this part. Now, if you want to learn how to handle the many-to-many relationships with additional information, check out the second part below:

Handle Many-To-Many relationship with JPA in a Spring Boot application - part 2
In this post, we will see how to handle Many-to-Many relationships that have additional information. We will use JPA, Hibernate, in a 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 ?