Previously, we have discussed one-to-many, many-to-one, and one-to-one relationships. Now it’s time for the many-to-many relationship. In this topic, we will analyze an example of how we can create this relationship using JPA.

Modeling tables

At the logical level, we usually imagine any relationship in the form of two tables with two connected entities. But, when it comes to a many-to-many relationship at the physical level, it represents two one-to-many relationships, which means another join table is created to connect the essential entity tables.

Let’s consider a situation where we should use a many-to-many relationship. There is an animal shelter that people visit to socialize with animals. The administration of the shelter keeps statistics on the interactions between people and animals in order to find the right owner for each animal as soon as possible. Each person can communicate with many animals and each animal can communicate with many people. It may be important for an animal how many hours a day a person is free. Also, it may be important for a person that an animal not ruin furniture. That’s why we need to include these columns in our tables. So, to build a many-to-many relationship between animals from the shelter and their potential owners, we will rely on these three tables:

The join table animal_person contains the animal_id and person_id columns. These columns are foreign keys that refer to the animal and person tables. Each column of the join table is a communication between a specific animal and a specific person.

Entity classes

Let’s start writing the code by creating our entities: Animal and Person classes. We will use the Lombok library to reduce the amount of boilerplate code.

The Animal class gets all its properties from the animal table: the idspeciesname and ruinsFurniture fields. From the Animal class, we need to be able to reach all the people the current animal object has been in contact with. For this purpose, we add the peopleInContact field with the @ManyToMany annotation. We don’t have to create another entity for the animal_person join table — JPA will work with our join table using the @JoinTable annotation. The "name=" parameter specifies the name of the join table. "joinColumns=" and "inverseJoinColumns=" define the columns of the join table: "joinColumns=" are columns with a foreign key that refer to the table the current class Animal represents, and "inverseJoinColumns=" refers to the inverse side — the person table.

JPA allows us to create many-to-many relationships without the @JoinTable annotation. In such cases, the names of the join table and its columns will be generated automatically.

import lombok.*;
import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Set;

@Getter @Setter
@NoArgsConstructor
@ToString(exclude="peopleInContact")
@Entity
public class Animal {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String species;
    private String name;
    private boolean ruinsFurniture;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
            name = "animal_person",
            joinColumns = @JoinColumn(name = "animal_id"),
            inverseJoinColumns = @JoinColumn(name = "person_id"))
    private Set<Person> peopleInContact = new LinkedHashSet<>();

    public Animal(String species, String name, boolean ruinsFurniture) {
        this.species = species;
        this.name = name;
        this.ruinsFurniture = ruinsFurniture;
    }
}

Many-to-many relationships can be either bidirectional or unidirectional. In our example, the relationship will be bidirectional, so just as the Animal entity knows about people, the Person entity should also know about animals. That’s why we need to use the animalsInContact field marked with the @ManyToMany annotation in the Person class. The "mappedBy=" parameter refers to the peopleInContact field of the Animal class. As you can see, in our example the Animal entity is the owning side and the Person is the inverse side.

import lombok.*;
import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Set;

@Getter @Setter
@NoArgsConstructor
@ToString(exclude="animalsInContact")
@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private int freeHours;

    @ManyToMany(mappedBy = "peopleInContact")
    private Set<Animal> animalsInContact = new LinkedHashSet<>();
    
    public Person(String name, int freeHours) {
        this.name = name;
        this.freeHours = freeHours;
    }
}

As you know, in a bidirectional many-to-one relationship, an entity can only be the owning side if it owns a foreign key (a property with the @ManyToOne annotation). It works a little differently for many-to-many relationships: you can choose which entity will be the owning side yourself.

Building of a unidirectional relationship differs from a bidirectional one in that only one entity (an owner side) contains a collection field to reference another entity. If we turn our example into a unidirectional relationship, the Animal class will remain the same, while the Person class will not have the animalsInContact field with the @ManyToMany annotation.

Accessing the database

Now that we’ve built a many-to-many relationship, let’s see it in action. In the EntityService class, we will create all our necessary methods for working with our entities. The first method insertEntities is responsible for creating Animal and Person objects and inserting them into the database:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import java.util.Set;

@Component
public class EntityService {

    private EntityManager entityManager;

    @Autowired
    public EntityService(EntityManagerFactory entityManagerFactory) {
        this.entityManager = entityManagerFactory.createEntityManager();
    }

    public void insertEntities() {
        entityManager.getTransaction().begin();

        Animal catLeo = new Animal("cat", "Leo", false);
        Animal dogCharlie = new Animal("dog", "Charlie", true);
        Animal dogBella = new Animal("dog", "Bella", false);

        Person catLover1 = new Person("James", 8);
        Person catLover2 = new Person("Mary", 6);
        Person dogLover1 = new Person("John", 4);

        catLeo.setPeopleInContact(Set.of(catLover1, catLover2));
        dogCharlie.getPeopleInContact().add(dogLover1);
        dogBella.getPeopleInContact().add(dogLover1);

        catLover1.getAnimalsInContact().add(catLeo);
        catLover2.getAnimalsInContact().add(catLeo);
        dogLover1.setAnimalsInContact(Set.of(dogCharlie, dogBella));

        entityManager.persist(catLeo);
        entityManager.persist(dogCharlie);
        entityManager.persist(dogBella);

        entityManager.getTransaction().commit();
        entityManager.clear();
    }
}

To access a database, we use the EntityManager interface from JPA. As the name implies, the EntityManager manages entities. Each EntityManager is associated with persistence context where entities are managed. In future topics, we’ll tell you more about EntityManager and persistence context, but for now, let’s focus on a few useful methods from EntityManager:

  • void persist(Object entity) adds an entity to a database;
  • void remove(Object entity) removes an entity from a database;
  • T find(Class<T> entityClass, Object primaryKey) returns an entity found by its key.

Since we use CascadeType.PERSIST in the Animal class, it is enough to only insert animals into the animal table. The person and join tables will be filled in automatically because each Animal object already contains Person objects in the peopleInContact field.

You can use CommandLineRunner in order to run methods from EntityService.

@Component
public class ShelterCommandLineRunner implements CommandLineRunner {

    @Autowired
    EntityService entityService;

    @Override
    public void run(String... args) {
        entityService.insertEntities();
        //other EntityService methods
    }
}

After running the code, JPA generates a query to insert an animal, then to insert all people associated with this animal, and then fills in the join table. For example, here are the queries for an animal named Leo:

INSERT INTO animal VALUES ("cat", "Leo", false);
INSERT INTO person VALUES ("James", 8);
INSERT INTO person VALUES ("Mary", 6);
INSERT INTO animal_person VALUES (1, 1);
INSERT INTO animal_person VALUES (1, 2);

Be careful when using CascadeType.REMOVE in a many-to-many relationship, as this will delete all records from all three tables associated with the entity chosen for deletion. For example, if we want to delete Leo from the animal table, all records with animal_id=1 will be deleted from animal_person table, as well as people contacted with Leo (James and Mary) will be deleted from the person table.

There is a difference between being the owning or the inverse side. The Animal class as the owning side can affect the join table when changing elements from the peopleInContact field. But the Person class can’t do this because its animalsInContact field is read-only. By adding and removing elements from the animalsInContact and peopleInContact fields we’ll see the difference. Let’s add some methods to EntityService class:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import java.util.Set;

@Component
public class EntityService {

    private EntityManager entityManager;

    @Autowired
    public EntityService(EntityManagerFactory entityManagerFactory) {
        this.entityManager = entityManagerFactory.createEntityManager();
    }

    //public void insertEntities()

    public void addPersonToSet() {
        entityManager.getTransaction().begin();

        Animal foundAnimal = entityManager.find(Animal.class, 2L);
        Person newDogLover = new Person("Emma", 5);

        // INSERT INTO person VALUES("Emma", 5);
        // INSERT INTO animal_person VALUES(2, 4)
        foundAnimal.getPeopleInContact().add(newDogLover);

        entityManager.getTransaction().commit();
        entityManager.clear();
    }

    public void deletePersonFromSet() {
        entityManager.getTransaction().begin();

        Animal foundAnimal = entityManager.find(Animal.class, 1L);
        Person firstPersonFromSet = foundAnimal.getPeopleInContact().iterator().next();

        // DELETE FROM animal_person
        // WHERE animal_id=1 and person_id=1
        foundAnimal.getPeopleInContact().remove(firstPersonFromSet);

        entityManager.getTransaction().commit();
        entityManager.clear();
    }

    public void addAnimalToSet() {
        entityManager.getTransaction().begin();

        Person foundPerson = entityManager.find(Person.class, 3L);
        Animal newDog = new Animal("dog", "Oscar", false);

        //doesn't generate a query
        foundPerson.getAnimalsInContact().add(newDog);

        entityManager.getTransaction().commit();
        entityManager.clear();
    }

    public void deleteAnimalFromSet() {
        entityManager.getTransaction().begin();

        Person foundPerson = entityManager.find(Person.class, 1L);
        Animal firstAnimalFromSet = foundPerson.getAnimalsInContact().iterator().next();

        //doesn't generate a query
        foundPerson.getAnimalsInContact().remove(firstAnimalFromSet);

        entityManager.getTransaction().commit();
        entityManager.clear();
    }
}

As you can see, the first two methods affect the join table as the peopleInContact field from the owning side Animal changes. And the last two methods don’t generate any queries for the join table because the animalsInContact field belongs to the Person class which is the inverse side.

Conclusion

A many-to-many relationship can be bidirectional and unidirectional. When the relationship is bidirectional, each of the two entity classes has a field with a collection (Set, List, etc.) to refer to each other. Each of such fields is marked with the @ManyToMany annotation. The @JoinTable annotation is used on the owning side to notify JPA of the join table, whereas the "mappedBy=" parameter is used on the inverse side. When the relationship is unidirectional, only one entity has a field with a collection that references another entity.

This topic provided you with a set of essential tools to get started with many-to-many relationships at a more advanced level. Now it’s time to practice!

Leave a Reply

Your email address will not be published.