Spring Data JPA 3 : Many-to-Many
Tu as vu dans la quêtes précédente les trois types d'associations : ici nous allons nous intéresser à Many-to-Many.
Cette association fait la jointure entre deux tables et peut se présenter sous deux formes :
- simple : une potion peut avoir plusieurs effets (n), un effet pouvant être attribué à plusieurs potions (n).
- avec attributs : une potion peut contenir plusieurs ingrédients (n), un ingrédient peut être utilisé dans plusieurs potions (n). Un ingrédient appartenant à une potion aura une certaine quantité.
:warning: En Spring Boot 3: le package des annotations JPA
javax.persistencea été transformée enjakarta.persistence
Note : l'IDE peut t'aider à trouver les packages de chaque élément
Objectifs
- Savoir créer une association Many-to-Many simple
- Savoir créer une association Many-to-Many avec attributs
Many-to-Many simple
Tu veux modéliser le cas suivant : une potion peut avoir plusieurs effets (n), un effet pouvant être attribué à plusieurs potions (n).
Tu as déjà créé les entités Effect et Potion :
@Entity
public class Effect {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
public Effect() {
}
// getters and setters omitted for brevity
}
@Entity
public class Potion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer power;
public Potion() {
}
// getters and setters omitted for brevity
}
La relation Many-to-Many que tu voudrais obtenir est modélisée de la façon suivante :

La jointure entre les deux tables se fait sur une table potion_effect, contenant le couple de clés étrangères potion_id et effect_id.
Dans la spécification JPA, une relation Many-to-Many se fait grâce à l'annotation @ManyToMany.
Tu vas devoir décider arbitrairement de l'entité qui contiendra le nom de la table de jointure et des clés étrangères. Je te conseille d'ajouter ces informations du côté qui te semble être le "maître" de la relation.
Si tu considères qu'il serait intéressant, pour une potion, d'obtenir la liste de ses effets : l'entité Potion serait le "maître" de la relation.
Tu utiliseras l'annotation @JoinTable en lui précisant :
-
le nom de la table de jointure, avec l'attribut
name -
la clé étrangère représentant la table actuelle, avec l'attribut
joinColumns -
la clé étrangère représentant la table à joindre, avec l'attribut
inverseJoinColumns
@Entity
public class Potion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer power;
@ManyToMany
@JoinTable(name = "potion_effect",
joinColumns = @JoinColumn(name = "potion_id"),
inverseJoinColumns = @JoinColumn(name = "effect_id"))
private List<Effect> effects = new ArrayList<>();
public Potion() {
}
// getters and setters omitted for brevity
}
Tu remarques que la classe
Potionva ainsi contenir une référence vers une liste deEffect: il faudra donc lui créer des accesseurs et mutateurs.
Lors de l'exécution du serveur Spring Boot, la table de jointure va ainsi être créée, ainsi que les contraintes de clés étrangères :
Hibernate: create table potion_effect (potion_id bigint not null, effect_id bigint not null) engine=InnoDB
Hibernate: alter table potion_effect add constraint FKji4l85mm0eu2i6jhh34kamhco foreign key (effect_id) references effect (id)
Hibernate: alter table potion_effect add constraint FKltnujustt578iw0lhwemwooic foreign key (potion_id) references potion (id)
Dans le code suivant, tu pourras ajouter des effets à une potion :
// create two effects
Effect shrink = effectRepository.save(new Effect("You shrink to half your size for 24 hours"));
Effect green = effectRepository.save(new Effect("Your skin turns green"));
// find the potion with id 1 (Long)
Potion potion = potionRepository.findById(1L).get();
// add two effects to the potion
potion.getEffects().add(shrink);
potion.getEffects().add(green);
// save the potion and create the foreign keys in `potion_effect`
potionRepository.save(potion);
Attention : les clés étrangères ne seront insérés dans la table potion_effect qu'au moment de la sauvegarde de la potion : potionRepository.save(potion);. Si tu te contente d'ajouter les effets sans enregistrer la potion, aucune modification n'aura lieu en base de données.
Hibernate: insert into potion_effect (potion_id, effect_id) values (?, ?)
Hibernate: insert into potion_effect (potion_id, effect_id) values (?, ?)
Il y aura autant d'insertions que d'effets associés à la potion.
Si une seule annotation @ManyToMany est présente, on parle de relation unidirectionnelle : il sera possible pour une potion de récupérer ses effets, mais un effet ne pourra pas récupérer l'ensemble de ses potions associées.
Dans le cas d'une relation bidirectionnelle, il te faudra ajouter l'annotation @ManyToMany dans la classe Effect.
De plus il faudra préciser l'attribut de Potion sur lequel la jointure sera faite (facile, c'est celui qui a l'annotation @ManyToMany) :
@Entity
public class Effect {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
@ManyToMany(mappedBy = "effects")
private List<Potion> potions = new ArrayList<>();
public Effect() {
}
// getters and setters omitted for brevity
}
Tu remarques que la classe
Effectva ainsi contenir une référence vers une liste dePotion: il faudra donc lui créer des accesseurs et mutateurs.
Ici mappedBy contient la valeur "effects" : elle fait référence à l'attribut effects de la classe Potion.
Comme la table de jointure existe déjà, aucune requête supplémentaire ne sera exécutée au lancement du serveur !
// find the effect with id 1 (Long)
Effect shrink = effectRepository.findById(1L).get();
// for each potion with shrink effect, show its name
for (Potion potion : shrink.getPotions()) {
System.out.println(potion.getName());
}
La documentation officielle sur l'annotation @JoinTable, avec un exemple
Un autre exemple de jointure Many-to-Many
Many-to-Many avec attributs
Tu veux maintenant modéliser le cas suivant : une potion peut contenir plusieurs ingrédients (n), un ingrédient peut être utilisé dans plusieurs potions (n). Chaque ingrédient appartenant à une potion aura une certaine quantité.
Tu as déjà créé les entités Potion et Ingredient :
@Entity
public class Ingredient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Ingredient() {
}
// getters and setters omitted for brevity
}
La relation Many-to-Many que tu voudrais obtenir est modélisée de la façon suivante :

Tu remarques que l'attribut quantity n'est ni présent dans l'entité Potion, ni dans l'entité Ingredient : il dépend de la jointure entre les deux.
Dans ce cas-là, on parle d'une relation Many-to-Many avec attributs.
Tu ne vas pas pouvoir utiliser l'annotation @ManyToMany car tu as remarqué précédemment qu'Hibernate créé lui-même la table de jointure.
Il va te falloir décomposer la relation Many-to-Many en plusieurs relations !
La première étape va être de créer une entité PotionIngredient qui va représenter la table de jointure et contenir l'attribut quantity.
Ensuite tu vas devoir relier cette entité à Potion et Ingredient en utilisant des annotations @OneToMany et @ManyToOne.
Commençons par voir à quoi ressemblera notre entité PotionIngredient :
@Entity
public class PotionIngredient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "potion_id")
private Potion potion;
@ManyToOne
@JoinColumn(name = "ingredient_id")
private Ingredient ingredient;
private Integer quantity;
public PotionIngredient() {
}
}
Les deux attributs potion et ingredient sont des clés étrangères jointes à leur entité respective grâce à l'annotation @ManyToOne, en précisant le nom de la clé dans la table de jointure.
Dans notre exemple, la table de jointure possède une clé primaire : c'est pour simplifier l'exercice. Tu pourrais créer une clé primaire composite à partir des deux clés étrangères potion_id et ingredient_id, mais compte tenu de la difficulté de l'opération, je te laisse le soin de regarder les ressources source le sujet si tu décides de te lancer dans l'aventure.
Au lancement du serveur, tu pourras lire les requêtes suivantes :
Hibernate: create table potion_ingredient (id bigint not null auto_increment, quantity integer, ingredient_id bigint, potion_id bigint, primary key (id)) engine=InnoDB
Hibernate: alter table potion_ingredient add constraint FKp8j3o5o7es1yfpu5ll12ra83t foreign key (ingredient_id) references ingredient (id)
Hibernate: alter table potion_ingredient add constraint FKboclq3wut0y7irqqee8ixuwof foreign key (potion_id) references potion (id)
Dans le code ci-dessous, tu va voir comment créer des données dans la table de jointure :
// find the potion with id 1 (Long)
Potion potion = potionRepository.findById(1L).get();
// create two ingredients
Ingredient sphinx = ingredientRepository.save(new Ingredient("Sphinx's Blood"));
Ingredient kobold = ingredientRepository.save(new Ingredient("Kobold Gland"));
// add the each ingredient to the potion with a specific quantity
potionIngredientRepository.save(new PotionIngredient(potion, sphinx, 12));
potionIngredientRepository.save(new PotionIngredient(potion, kobold, 4));
La jointure ne sera sauvegardée qu'à l'appel de la méthode
savesur le repository correspondant à l'entitéPotionIngredient
Hibernate: insert into potion_ingredient (ingredient_id, potion_id, quantity) values (?, ?, ?)
Hibernate: insert into potion_ingredient (ingredient_id, potion_id, quantity) values (?, ?, ?)
Qui dit @ManyToOne sous-entends @OneToMany (sauf si tu ne veux pas d'une relatation bidirectionnelle). Tu vas devoir modifier les entités Potion et Ingredient de la façon suivante :
@Entity
public class Potion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer power;
@OneToMany(mappedBy = "potion", cascade = CascadeType.ALL)
private List<PotionIngredient> potionIngredients = new ArrayList<>();
// getters and setters omitted for brevity
}
@Entity
public class Ingredient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "ingredient", cascade = CascadeType.ALL)
private List<PotionIngredient> potionIngredients = new ArrayList<>();
// getters and setters omitted for brevity
}
Ainsi pour une potion, tu pourras afficher la liste de ses ingrédients :
// find the potion with id 1 (Long)
Potion potion = potionRepository.findById(1L).get();
// for each potion, get the junctures with ingredient
for (PotionIngredient potionIngredient : potion.getPotionIngredients()) {
// get the ingredient from each juncture and show its name
System.out.println(potionIngredient.getIngredient().getName());
}
Tu remarques dans le code précédent que dans le cas d'une relation many-to-many avec attributs, il faut créer un repository pour accéder à la table de jointure :
@Repository
public interface PotionIngredientRepository extends JpaRepository<PotionIngredient, Long> {
}
Lire l'étape "4. Many-to-Many With a New Entity" pour voir l'exemple d'une relation où on peut avoir plusieurs fois la même jointure entre deux tables (ex: une personne voit plusieurs fois le même film, mais à des dates différentes)
Un exemple de Many-to-Many avec attributs, utilisant une clé primaire composite
Définition d'une clé primaire composite et différentes façon de les implémenter
Démonstration
Afin de voir et de tester les exemples de cette quête, récupère le dépôt en ressource.
Si tu ne l'as pas fait dans la quête précédente, crée une base de données spring_jpa_sample :
CREATE DATABASE IF NOT EXISTS `spring_jpa_sample`;
L'utilisateur est le même que celui des quêtes précédentes : h4rryp0tt3r avec le mot de passe Horcrux4life!.
S'il ne les a pas déjà, il faudra lui donner tous les droits sur la base de données spring_jpa_sample.
Challenge
La magie de Spring Data JPA
Fait un fork du dépôt suivant : https://github.com/WildCodeSchool/quest-spring-jpa-manytomany.
Tu reprends le développement d'un site de fan d'Harry Potter afin d'y ajouter les fonctionnalités suivantes :
-
un sorcier devrait pouvoir s'inscrire à un ou plusieurs cours.
-
il devrait être possible de récupérer la liste des cours où un élève est inscrit.
La première étape va consister à créer la base de données spring_jpa_manytomany, à partir du fichier sql suivant.
L'utilisateur est le même que celui des quêtes précédente : h4rryp0tt3r avec le mot de passe Horcrux4life!.
Il faudra lui donner tous les droits sur la base de données spring_jpa_manytomany.
-
Modifier l'entité
Wizardafin d'y ajouter le code nécessaire à la création de sa relation avec une liste de cours :List<Course> courses. -
Wizardsera le "maître" de la relation et la table de jointure s'appellerawizard_course. -
Modifier l'entité
Courseafin d'y ajouter le code nécessaire à la création de sa relation avec une liste de sorciers :List<Wizard> wizards -
Relancer le serveur : les requêtes de création de la table de jointure, des clés étrangères de leurs contraintes devraient apparaître dans les logs
-
Sans modifier d'autres classes, il devrait être possible d'ajouter des cours à un élève et de voir la liste des cours où il s'est inscrit, à partir du lien "courses"
Si ça ne marche pas, regarde les messages d'erreurs dans les logs :
-
as-tu bien nommé les attributs comme demandé ?
-
as-tu créé les getters et setters ?
Critères de validation
- Le projet s'exécute bien sur la base de données et l'utilisateur requis.
- La classe
Wizardcontient bien tout le code nécessaire pour lier une liste de cours. - La classe
Coursecontient bien tout le code nécessaire pour lier une liste d'élèves. - Il est possible pour un élève de s'inscrire à un ou plusieurs cours à partir du lien "courses".
- Il est possible de voir la liste des cours d'un élève à partir du lien "courses".
- Le code est disponible sur GitHub.
Ressources partagées sur cette quête
- Java Code Geeks offre des exemples de code et des explications détaillées pour les relations many-to-many en JPA.https://www.javacodegeeks.com/2019/04/hibernate-many-many-tutorial.html?amp=1