Vous êtes toujours avec moi, félicitations ! Nous venons de voir des notions assez denses, et je reconnais que c’est un vrai challenge de les appréhender. Nous n’avons pas encore fini, cependant.
Précédemment, je vous ai fait implémenter des relations unidirectionnelles tout en vous indiquant qu’il existe des relations bidirectionnelles. Nous allons voir dans quel cas elles sont utiles, et comment les implémenter.
Dans le chapitre précédent, nous avons implémenté le code nécessaire pour :
récupérer une catégorie et tous ses produits associés ;
récupérer un produit et les commentaires associés.
Vous apprenez désormais que sur l’interface qui affichera le produit et les commentaires associés, il est également nécessaire d’y présenter les catégories ! Et nous voilà donc dans un cas typique de bidirectionnalité !
La catégorie doit connaître les produits, mais le produit doit aussi connaître les catégories.
Reprenons notre code et implémentons cette relation bidirectionnelle !
Implémentez une relation bidirectionnelle @ManyToMany
Pour implémenter la bidirectionnalité, nous devons modifier la classe Product, étant donné que la classe Category possède déjà la référence aux produits. Je vous montre comment faire dans le screencast qui suit :
Reprenons le code :
package com.openclassrooms.datalayer.model;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "produit")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "produit_id")
private int productId;
@Column(name = "nom")
private String name;
@Column(name = "description")
private String description;
@Column(name = "cout")
private int cost;
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.EAGER)
@JoinColumn(name = "produit_id")
List<Comment> comments = new ArrayList<>();
@ManyToMany(
mappedBy = "products"
)
private List<Category> categories = new ArrayList<>();
// Ajouter les getters & setters
}
L’ajout de l’attribut categories est relativement simple. L’annotation @ManyToMany est de rigueur, et l’unique propriété à utiliser est mappedBy. Cette propriété est valide dans le contexte de la bidirectionnalité, car elle indique le nom de l’attribut dans l’autre entité.
Étant donné que dans la classe Category, l’annotation @ManyToMany est apposée sur l’attribut nommé “products”, alors la valeur de la propriété mappedBy est “products”.
Et c’est tout ! Lorsqu'une relation unidirectionnelle est déjà en place, la bidirectionnalité n’est pas complexe.
À vous de jouer !
Je vous propose l’exercice suivant : modifier le comportement de la méthode run du DataLayerApplication pour prouver la validité de la bidirectionnalité. Votre code doit donc :
Récupérer 1 catégorie et afficher les produits associés.
Récupérer 1 produit et afficher les catégories associées.
Pour la correction, vous pouvez consulter le repository à la branche p2c4.
Implémentez une relation bidirectionnelle @OneToMany / @ManyToOne
Continuons sur le thème de la bidirectionnalité en nous intéressant à l’association Product-Comment.
Dans le chapitre précédent, nous avons implémenté une relation unidirectionnelle de type OneToMany entre Product et Comment. Transformer cette relation en une relation bidirectionnelle implique de modifier l’entité Comment, afin que cette dernière ait connaissance de son produit associé.
La spécificité de cette bidirectionnalité est qu’il ne s’agit pas d’un OneToMany côté Comment mais d’un ManyToOne, car plusieurs commentaires peuvent être ajoutés au même produit.
Allons modifier la classe Comment :
package com.openclassrooms.datalayer.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "commentaire")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="commentaire_id")
private int commentId;
@Column(name="contenu")
private String content;
@ManyToOne(
cascade = CascadeType.ALL
)
@JoinColumn(name="produit_id")
private Product product;
// Ajouter getter & setter
}
Cette annotation se rapproche du @OneToMany, dans la mesure où on lui associe également un @JoinColumn. La valeur de la propriété name du @JoinColumn est la clé étrangère dans la table Comment, en l'occurrence produit_id.
Si vous testez, vous verrez qu’il fonctionne ! Mais en réalité nous n’avons pas créé une relation bidirectionnelle, nous avons 2 relations unidirectionnelles.
Pourquoi, me direz-vous ? Car dans une relation bidirectionnelle, seulement un des deux côtés doit faire le lien avec la relation en base de données avec le @JoinColumn. Or actuellement, les 2 entités définissent le @JoinColumn.
Pour que notre relation soit bidirectionnelle, nous devons modifier la classe Product et les annotations de l’attribut comments :
package com.openclassrooms.datalayer.model;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "produit")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "produit_id")
private int productId;
@Column(name = "nom")
private String name;
@Column(name = "description")
private String description;
@Column(name = "cout")
private int cost;
@OneToMany(
mappedBy = "product",
cascade = CascadeType.ALL,
orphanRemoval = true
)
List<Comment> comments = new ArrayList<>();
@ManyToMany(
mappedBy = "produits",
)
private List<Category> categories = new ArrayList<>();
// Ajouter getters & setters
}
Il est à noter que :
L’annotation @JoinColumn sur l’attribut comments a été enlevée.
La propriété mappedBy = “product” dans l’annotation @OneToMany a été ajoutée.
Le fetch = FetchType.EAGER a également été enlevé. Je laisse la valeur par défaut LAZY.
Comme pour la relation bidirectionnelle en ManyToMany, le mappedBy permet de faire référence à l’attribut dans la seconde entité.
Mais pourquoi ne pas mettre le mappedBy du côté de l’entité Comment ?
Car Spring Data JPA ne le permet pas, c’est forcément le côté @ManyToOne qui doit être maître de la relation et avoir le @JoinColumn.
Pour conclure, une bonne pratique dans le contexte des relations bidirectionnelles est d’ajouter des méthodes utilitaires (dites helpers methods, en anglais) pour aider à la synchronisation de nos objets. Au sein de la classe Product, voici les 2 méthodes en question :
public void addComment(Comment comment) {
comments.add(comment);
comment.setProduct(this);
}
public void removeComment(Comment comment) {
comments.remove(comment);
comment.setProduct(null);
}
Il s’agit ici de méthodes utilitaires pratiques pour la manipulation de vos entités.
Pour une relation bidirectionnelle :
OneToMany/ManyToOne : elles seront du côté OneToMany (là où on gère une liste d’éléments).
ManyToMany : elles seront bien souvent du côté de l’entité qui gère la relation (celle où il y a le @JoinTable). Dans le cas de l’application Carlib Assurances, ce sera donc dans la classe Category comme ci-dessous :
public void addProduct(Product product) {
products.add(product);
product.getCategories().add(this);
}
public void removeProduit(Product product) {
products.remove(product);
product.getCategories().remove(this);
}
En vue des actions à réaliser durant le chapitre à venir, je vous demande également d’ajouter un CascadeType.ALL à la relation @ManyToMany dans la classe Product sur l’attribut categories.
@ManyToMany(
mappedBy = "products",
cascade = CascadeType.ALL
)
private List<Category> categories = new ArrayList<>();
À vous de jouer !
Une fois n’est pas coutume, modifiez le comportement de la méthode run du DataLayerApplication pour prouver la validité de la bidirectionnalité. Votre code doit donc :
Récupérer 1 produit et afficher les commentaires associés.
Récupérer 1 commentaire et afficher le produit associé.
En résumé
Une relation bidirectionnelle implique que les 2 entités aient connaissance l’une de l’autre.
Les paires d’annotations possibles sont :
@OneToMany - @ManyToOne
@ManyToMany - @ManyToMany
@OneToOne - @OneToOne
Lors des relations bidirectionnelles, un fonctionnement en LAZY est à favoriser.
Notre couche de gestion de la persistance des données (data layer) commence vraiment à prendre forme. Mais une application ne se limite pas à lire de la donnée. Il est également nécessaire de créer, modifier et supprimer des données. Pour découvrir comment Spring Data JPA nous permet de mener à bien ces opérations, suivez-moi dans le prochain chapitre !