Java persistence with JPA and Hibernate, Part 2: Many-to-many relationships

Many-to-many relationships and cascade type strategies in JPA and Hibernate.

JavaWorld > Persistence [series] > data blocks / structure / database / data center
RamCreativ / Getty Images

The first half of this tutorial introduced fundamentals of the Java Persistence API and showed you how to configure a JPA application using Hibernate 5.3.6 and Java 8. If you've read that tutorial and studied its example application, then you know the basics of modeling JPA entities and many-to-one relationships in JPA. You've also had some practice writing named queries with JPA Query Language (JPQL).

In this second half of the tutorial we'll go deeper with JPA and Hibernate. You'll learn how to model a many-to-many relationship between Movie and SuperHero entities, set up individual repositories for these entities, and persist the entities to the H2 in-memory database. You'll also learn more about the role of cascade operations in JPA, and get tips for choosing a CascadeType strategy for entities in the database. Finally, we'll pull together a working application that you can run in your IDE or on the command line.

This tutorial focuses on JPA fundamentals, but be sure to check out these Java tips introducing more advanced topics in JPA:

download
Download the source code for example applications used in this tutorial. Created by Steven Haines for JavaWorld.

Many-to-many relationships in JPA

Many-to-many relationships define entities for which both side of the relationship can have multiple references to each other. For our example, we're going to model movies and superheroes. Unlike the Authors & Books example from Part 1, a movie can have multiple superheroes, and a superhero can appear in multiple movies. Our superheroes, Ironman and Thor, both appear in two movies, "The Avengers" and "Avengers: Infinity War."

To model this many-to-many relationship using JPA, we will need three tables:

  • MOVIE
  • SUPER_HERO
  • SUPERHERO_MOVIES

Figure 1 shows the domain model with the three tables.

Domain model for a many-to-many relationship in JPA. Steven Haines

Figure 1. Domain model for movies and superheroes

Note that SuperHero_Movies is a join table between the Movie and SuperHero tables. In JPA, a join table is a special kind of table that facilitates the many-to-many relationship.

Unidirectional or bidirectional?

In JPA we use the @ManyToMany annotation to model many-to-many relationships. This type of relationship can be unidirectional or bidirectional:

  • In a unidirectional relationship only one entity in the relationship points the other.
  • In a bidirectional relationship both entities point to each other.

Our example is bidirectional, meaning that a movie points to all of its superheroes, and a superhero points to all of of their movies. In a bidirectional, many-to-many relationship, one entity owns the relationship and the other is mapped to the relationship. We use the mappedBy attribute of the @ManyToMany annotation to create this mapping.

Listing 1 shows the source code for the SuperHero class.

Listing 1. SuperHero.java


package com.geekcap.javaworld.jpa.model;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Table(name = "SUPER_HERO")
public class SuperHero {
    @Id
    @GeneratedValue
    private Integer id;
    private String name;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
    @JoinTable(
            name = "SuperHero_Movies",
            joinColumns = {@JoinColumn(name = "superhero_id")},
            inverseJoinColumns = {@JoinColumn(name = "movie_id")}
    )
    private Set<Movie> movies = new HashSet<>();

    public SuperHero() {
    }

    public SuperHero(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public SuperHero(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Movie> getMovies() {
        return movies;
    }

    @Override
    public String toString() {
        return "SuperHero{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", movies='" + movies.stream().map(Movie::getTitle).collect(Collectors.toList()) + '\'' +
                '}';
    }
}

The SuperHero class has a couple of annotations that should be familiar from Part 1:

  • @Entity identifies SuperHero as a JPA entity.
  • @Table maps the SuperHero entity to the "SUPER_HERO" table.

Also note the Integer id field, which specifies that the table's primary key will be automatically generated.

Next we'll look at the @ManyToMany and @JoinTable annotations.

Fetching strategies

The thing to notice in the @ManyToMany annotation is how we configure the fetching strategy, which can be lazy or eager. In this case, we've set the fetch to EAGER, so that when we retrieve a SuperHero from the database, we'll also automatically retrieve all of its corresponding Movies.

If we chose to perform a LAZY fetch instead, we would only retrieve each Movie as it was specifically accessed. Lazy fetching is only possible while the SuperHero is attached to the EntityManager; otherwise accessing a superhero's movies will throw an exception. We want to be able to access a superhero's movies on demand, so in this case we choose the EAGER fetching strategy.

Join tables

JoinTable is a class that facilitates the many-to-many relationship between SuperHero and Movie. In this class, we define the table that will store the primary keys for both the SuperHero and the Movie entities.

Listing 1 specifies that the table name will be SuperHero_Movies. The join column will be superhero_id, and the inverse join column will be movie_id. The SuperHero entity owns the relationship, so the join column will be populated with SuperHero's primary key. The inverse join column then references the entity on the other side of the relationship, which is Movie.

Based on these definitions in Listing 1, we would expect a new table created, named SuperHero_Movies. The table will have two columns: superhero_id, which references the id column of the SUPERHERO table, and movie_id, which references the id column of the MOVIE table.

The Movie class

Listing 2 shows the source code for the Movie class. Recall that in a bidirectional relationship, one entity owns the relationship (in this case, SuperHero) while the other is mapped to the relationship. The code in Listing 2 includes the relationship mapping applied to the Movie class.

Listing 2. Movie.java


package com.geekcap.javaworld.jpa.model;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "MOVIE")
public class Movie {

    @Id
    @GeneratedValue
    private Integer id;
    private String title;

    @ManyToMany(mappedBy = "movies", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)
    private Set<SuperHero> superHeroes = new HashSet<>();

    public Movie() {
    }

    public Movie(Integer id, String title) {
        this.id = id;
        this.title = title;
    }

    public Movie(String title) {
        this.title = title;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Set<SuperHero> getSuperHeroes() {
        return superHeroes;
    }

    public void addSuperHero(SuperHero superHero) {
        superHeroes.add(superHero);
        superHero.getMovies().add(this);
    }

    @Override
    public String toString() {
        return "Movie{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

The following properties are applied to the @ManyToMany annotation in Listing 2:

  • mappedBy references the field name on the SuperHero class that manages the many-to-many relationship. In this case, it references the movies field, which we defined in Listing 1 with the corresponding JoinTable.
  • cascade is configured to CascadeType.PERSIST, which means that when a Movie is saved its corresponding SuperHero entities should also be saved.
  • fetch tells the EntityManager that it should retrieve a movie's superheroes eagerly: when it loads a Movie, it should also load all corresponding SuperHero entities.

Something else to note about the Movie class is its addSuperHero() method.

When configuring entities for persistence, it isn't enough to simply add a superhero to a movie; we also need to update the other side of the relationship. This means we need to add the movie to the superhero. When both sides of the relationship are configured properly, so that the movie has a reference to the superhero and the superhero has a reference to the movie, then the join table will also be properly populated.

We've defined our two entities. Now let's look at the repositories we'll use to persist them to and from the database.

JPA repositories

We could implement all of our persistence code directly in the sample application, but creating repository classes allows us to separate persistence code from application code. Just like we did with the Books & Authors application in Part 1, we'll create an EntityManager and then use it to initialize two repositories, one for each entity we're persisting.

Listing 3 shows the source code for the MovieRepository class.

Listing 3. MovieRepository.java


package com.geekcap.javaworld.jpa.repository;

import com.geekcap.javaworld.jpa.model.Movie;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class MovieRepository {
    private EntityManager entityManager;

    public MovieRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Optional<Movie> save(Movie movie) {
        try {
            entityManager.getTransaction().begin();
            entityManager.persist(movie);
            entityManager.getTransaction().commit();
            return Optional.of(movie);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Optional.empty();
    }

    public Optional<Movie> findById(Integer id) {
        Movie movie = entityManager.find(Movie.class, id);
        return movie != null ? Optional.of(movie) : Optional.empty();
    }

    public List<Movie> findAll() {
        return entityManager.createQuery("from Movie").getResultList();
    }

    public void deleteById(Integer id) {
        // Retrieve the movie with this ID
        Movie movie = entityManager.find(Movie.class, id);
        if (movie != null) {
            try {
                // Start a transaction because we're going to change the database
                entityManager.getTransaction().begin();

                // Remove all references to this movie by superheroes
                movie.getSuperHeroes().forEach(superHero -> {
                    superHero.getMovies().remove(movie);
                });

                // Now remove the movie
                entityManager.remove(movie);

                // Commit the transaction
                entityManager.getTransaction().commit();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

The MovieRepository is initialized with an EntityManager, then saves it to a member variable to use in its persistence methods. We'll consider each of these methods.

Persistence methods

Let's review MovieRepository's persistence methods and see how they interact with the EntityManager's persistence methods.

1 2 3 Page 1
Page 1 of 3