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 row 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 id, species, name 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.
Java
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;
}
}Kotlin
import javax.persistence.*
import java.util.LinkedHashSet
import java.util.Set
@Entity
class Animal(
var species: String = "",
var name: String = "",
var ruinsFurniture: Boolean = false
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long = 0
@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE])
@JoinTable(
name = "animal_person",
joinColumns = [JoinColumn(name = "animal_id")],
inverseJoinColumns = [JoinColumn(name = "person_id")]
)
var peopleInContact: MutableSet<Person> = LinkedHashSet<Person>()
}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.
Java
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;
}
}Kotlin
import javax.persistence.*
import java.util.LinkedHashSet
import java.util.Set
@Entity
class Person(var name: String = "", var freeHours: Int = 0) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0
@ManyToMany(mappedBy = "peopleInContact")
var animalsInContact: MutableSet<Animal> = LinkedHashSet()
}
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:
Java
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();
}
}Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import javax.persistence.EntityManager
import javax.persistence.EntityManagerFactory
@Component
class EntityService(@Autowired entityManagerFactory: EntityManagerFactory) {
private val entityManager: EntityManager
init {
entityManager = entityManagerFactory.createEntityManager()
}
fun insertEntities() {
entityManager.transaction.begin()
val catLeo = Animal("cat", "Leo", false)
val dogCharlie = Animal("dog", "Charlie", true)
val dogBella = Animal("dog", "Bella", false)
val catLover1 = Person("James", 8)
val catLover2 = Person("Mary", 6)
val dogLover1 = Person("John", 4)
catLeo.peopleInContact = mutableSetOf(catLover1, catLover2)
dogCharlie.peopleInContact.add(dogLover1)
dogBella.peopleInContact.add(dogLover1)
catLover1.animalsInContact.add(catLeo)
catLover2.animalsInContact.add(catLeo)
dogLover1.animalsInContact = mutableSetOf(dogCharlie, dogBella)
entityManager.persist(catLeo)
entityManager.persist(dogCharlie)
entityManager.persist(dogBella)
entityManager.transaction.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:
Java
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.
Kotlin
persist(entity: Any): Unitadds an entity to a database;remove(entity: Any): Unitremoves an entity from a database;find(entityClass: Class<T>, primaryKey: Any): Treturns 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.
Java
@Component
public class ShelterCommandLineRunner implements CommandLineRunner {
@Autowired
EntityService entityService;
@Override
public void run(String... args) {
entityService.insertEntities();
//other EntityService methods
}
}Kotlin
@Component
class ShelterCommandLineRunner(@Autowired val entityService: EntityService) : CommandLineRunner {
override fun run(vararg args: String) {
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.
The reason for this difference lies in how JPA manages and persists changes to the relationship. The owning side is responsible for managing the relationship and updating the join table that holds the association between the two entities. When you add or remove elements from the peopleInContact field in the Animal class, JPA will automatically update the join table to reflect these changes. This is because the owning side controls the foreign key relationship in the join table.
On the other hand, the inverse side provides a read-only view of the relationship. It reflects the relationship but does not manage or persist changes to it. So, modifying the animalsInContact field in the Person class will not result in updates to the join table, as the inverse side does not control the relationship.
By adding and removing elements from the animalsInContact and peopleInContact fields we can see this difference. So, let's add some methods to EntityService class for this:
Java
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();
}
}Kotlin
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
@Component
class EntityService (@Autowired entityManagerFactory: EntityManagerFactory) {
private val entityManager: EntityManager
init {
entityManager = entityManagerFactory.createEntityManager()
}
//fun insertEntities(): Unit
fun addPersonToSet() {
entityManager.transaction.begin()
val foundAnimal = entityManager.find(Animal::class.java, 2L)
val newDogLover = Person("Emma", 5)
// INSERT INTO person VALUES("Emma", 5);
// INSERT INTO animal_person VALUES(2, 4)
foundAnimal.peopleInContact.add(newDogLover)
entityManager.transaction.commit()
entityManager.clear()
}
fun deletePersonFromSet() {
entityManager.transaction.begin()
val foundAnimal = entityManager.find(Animal::class.java, 1L)
val firstPersonFromSet = foundAnimal.peopleInContact.iterator().next()
// DELETE FROM animal_person
// WHERE animal_id=1 and person_id=1
foundAnimal.peopleInContact.remove(firstPersonFromSet)
entityManager.transaction.commit()
entityManager.clear()
}
fun addAnimalToSet() {
entityManager.transaction.begin()
val foundPerson = entityManager.find(Person::class.java, 3L)
val newDog = Animal("dog", "Oscar", false)
//doesn't generate a query
foundPerson.animalsInContact.add(newDog)
entityManager.transaction.commit()
entityManager.clear()
}
fun deleteAnimalFromSet() {
entityManager.transaction.begin()
val foundPerson = entityManager.find(Person::class.java, 1L)
val firstAnimalFromSet = foundPerson.animalsInContact.iterator().next()
//doesn't generate a query
foundPerson.animalsInContact.remove(firstAnimalFromSet)
entityManager.transaction.commit()
entityManager.clear()
}
}As shown in the code snippet above, the first two methods modify the join table because they involve changes to the peopleInContact field in the owning side, Animal. In contrast, the last two methods do not generate any queries for the join table, as they interact with the animalsInContact field in the Person class, which is the inverse side.
Conclusion
A many-to-many relationship can be either bidirectional or 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!