• 10 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/03/2022

Utilisez les relations unidirectionnelles

Dans le cadre de votre mission pour Carlib Assurances, vous avez en partie implémenté une fonctionnalité pour afficher les produits d’assurance auto.

Cette fonctionnalité d’affichage a été implémentée dans le chapitre précédent.

Vous étiez satisfait de votre travail, quand soudain, on vous informe dans l’oreillette que l’affichage unitaire d’un produit souffre d’un manque d’informations. 

Pourtant on a bien récupéré toutes les informations du produit ?

C’est vrai ! Mais l’affichage d’un produit va au-delà des informations propres à ce dernier. La fonctionnalité implique d’afficher le produit, mais également les commentaires associés.

Et ceci est un exemple parfait de relation ! L’entité Product est associée à l’entité Comment dans la mesure où un produit peut avoir plusieurs commentaires.

Cette notion de relation trouve son pendant au sein de la base de données.

Je vous conseille très fortement de le lire si vous n’êtes pas familiarisé avec les relations en base de données ! 

Voici un récapitulatif des éléments clés :

Nous le disions au préalable, ces relations concernent nos entités donc notre code objet. Ainsi, la première chose à savoir est que ces relations sont modélisables dans vos entités grâce aux annotations @OneToOne, @OneToMany, @ManyToOne,@ManyToMany. Et nous allons voir comment les utiliser.

Mais pas si vite ! Je me dois de vous présenter la notion de directionnalité avant de vous montrer concrètement le code. Il en existe 2 : l’unidirectionnalité et la bidirectionnalité. Laissez-moi vous expliquer.

Une relation concerne 2 objets ! Par exemple, un produit peut avoir plusieurs commentaires, mais un commentaire est associé à un seul produit. Il s’agit ici d’une relation 1 à plusieurs.

Ah, ok !  Comment est-ce que cette relation sera implémentée dans le code ?

Idéalement, il nous faut un attribut dans la classe Product. Cet attribut sera la liste des commentaires associés au produit. Dans la classe Comment, il n’y aucune référence vers le produit. C’est donc une relation unidirectionnelle !

Le produit connaît les commentaires, mais la réciproque n’est pas vraie.

Supposons maintenant que l’on désire avoir un attribut dans la classe Comment qui correspond au Product. Alors dans ce cas, la classe Product fait référence aux commentaires, et la classe Comment fait référence au produit. Nous obtenons une relation bidirectionnelle.

Par expérience, dans la majorité des cas, la relation unidirectionnelle sera suffisante. Mais parfois, il est bien utile de mettre en place la bidirectionnalité.

Implémentez une relation unidirectionnelle @OneToMany

Commençons par implémenter une relation unidirectionnelle entre Product et Comment, comme dans l’exemple ci-dessus.

Dans le screencast qui suit, je vous montre comment faire.

Nous avons donc modifié la classe Product comme ceci :

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.OneToMany;
import javax.persistence.Table;
 
@Entity
@Table(name = "produit")
public class Product {
 
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="produit_id")
	private int produitId;
	
	@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<>();
	
	// Add getters & setters
	
}

Notons avec intérêt le nouvel attribut ‘comments’. 2 annotations y sont associées :

  • @OneToMany : cette annotation permet de spécifier une relation ‘une à plusieurs’. Pour un produit, il y a plusieurs commentaires possibles.

    • La propriété ‘cascade’ permet de définir quel impact l’action sur une entité aura sur son entité associée. Le type ‘ALL’ signifie que toutes les actions sur l’entité Produit seront propagées sur l’entité Commentaires. Exemple : si on supprime le produit, les commentaires associés seront également supprimés.

    • La propriété orphanRemoval=true permet d’activer un mécanisme qui garantit la non-existence de commentaire orphelin de son produit. Si on supprime un commentaire de la liste des commentaires du Product, alors le commentaire devient orphelin, et il est supprimé de la base de données.

    • La propriété fetch possède la valeur EAGER, et cela signifie qu’à la récupération du produit, tous les commentaires seront également récupérés.

  • @JoinColumn : Cette annotation permet d’indiquer le nom de la clé étrangère dans la table de l’entité concernée.

Vous pouvez désormais tester ce code et cela sans aucune modification du ProductService. En effet, la modification que nous avons faite aura un impact sur la création de l’objet Product lors de l’appel des méthodes du Repository. Or, la création est gérée par Spring Data JPA, donc nous n’avons rien d'autre à faire dans notre code, génial non ?! 

À vous de jouer !

Implémentez le code que je vous ai montré, puis testez-le en récupérant un produit et en affichant le contenu de chaque commentaire associé.

Avez-vous réussi ? Voici une correction pour la classe DataLayerApplication :

package com.openclassrooms.datalayer;
 
import java.util.Optional;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
import com.openclassrooms.datalayer.model.Product;
import com.openclassrooms.datalayer.service.ProductService;
 
@SpringBootApplication
public class DataLayerApplication implements CommandLineRunner {
 
	@Autowired
	private ProductService productService;
 
	public static void main(String[] args) {
		SpringApplication.run(DataLayerApplication.class, args);
	}
 
	@Override
	public void run(String... args) throws Exception {
 
		Optional<Product> optProduct = productService.getProductById(1);
		Product productId1 = optProduct.get();
 
		System.out.println(productId1.getName());	
 
		productId1.getComments().forEach(
				comment -> System.out.println(comment.getContent()));	
				
	}		
}

 Le code est également disponible sur le repository du cours, branche p2c3.

Implémentez une relation unidirectionnelle @ManyToMany

L’application Carlib Assurances nous confronte à un autre type de relation. En effet, l’une des fonctionnalités du produit consiste à afficher la liste des catégories, et vous avez implémenté la récupération des catégories dans le chapitre 2 de cette partie de cours. 

Simplement voilà, afficher le nom des catégories n’est pas suffisant ! L’objectif est de récupérer les produits de chaque catégorie.

Trop facile ! Il suffit de faire comme dans la section précédente, non ?

Pas tout à fait, car il ne s’agit pas ici d’une relation un à plusieurs, mais d’une relation plusieurs à plusieurs. Car une catégorie peut être associée à plusieurs produits, et un produit peut être rattaché à plusieurs catégories !

Je vais donc vous apprendre à utiliser l’annotation @ManyToMany. Are you ready ? 

Comme précédemment, je commence par vous montrer l’utilisation de l’annotation et en l'occurrence, c’est dans la classe Category :

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.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
 
@Entity
@Table(name = "categorie")
public class Category {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="categorie_id")
	private int categoryId;
	
	@Column(name="nom")
	private String name;
 
	@ManyToMany(
fetch = FetchType.LAZY,
	cascade = { 
			CascadeType.PERSIST, 
			CascadeType.MERGE 
			}	
	)
	@JoinTable(
			name = "categorie_produit",
			joinColumns = @JoinColumn(name = "categorie_id"), 	
			inverseJoinColumns = @JoinColumn(name = "produit_id")
	)
	private List<Product> products = new ArrayList<>();	
}

Des explications sont nécessaires !

Nous utilisons 2 propriétés de @ManyToMany :

  • La propriété fetch possède la valeur LAZY, et cela signifie qu’à la récupération de la catégorie, les produits ne sont pas récupérés. Par voie de conséquence, les performances sont meilleures (la requête est plus légère) ; cependant, lorsqu'ultérieurement dans votre code vous accéderez aux produits à partir de l’objet Catégorie en question, une nouvelle requête sera exécutée. 

  • Cascade : contrairement aux exemples précédents, nous ne voulons pas un CascadeType.ALL qui impliquerait une cascade dans le cas de la suppression. Je spécifie donc uniquement PERSIST et MERGE, la cascade s’applique donc tant en création qu’en modification.

Il est également nécessaire d’utiliser l’annotation @JoinTable avec les propriétés suivantes :

  • name : correspond au nom de la table de jointure en base de données ;

  • joinColumns correspond à la clé étrangère dans la table de jointure ;

  • inverseJoinColumns correspond à la clé étrangère dans la table de jointure de la seconde entité concernée par la relation.

La classe CategoryService n’a pas à être modifiée, mais nous devons désormais adapter le code de DataLayerApplication :

import javax.transaction.Transactional;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
import com.openclassrooms.datalayer.model.Category;
import com.openclassrooms.datalayer.service.CategoryService;
 
@SpringBootApplication
public class DataLayerApplication implements CommandLineRunner {
 
	@Autowired
	private CategoryService categoryService;
 
	public static void main(String[] args) {
		SpringApplication.run(DataLayerApplication.class, args);
	}
 
	@Override
	@Transactional
	public void run(String... args) throws Exception {
 
		Optional<Category> optCategory = categoryService.getCategoryById(1);
		Category categoryId1 = optCategory.get();
		
		System.out.println(categoryId1.getName());	
 
		categoryId1.getProducts().forEach(
				product -> System.out.println(product.getName()));			
	}		
}

 Si le contenu de la méthode run n’a rien de nouveau, j’attire votre attention sur la ligne 26 et l’annotation @Transactional.

C’est l’occasion parfaite pour moi pour vous parler d’un point important : les transactions.

Les transactions sont le mécanisme qui permet de respecter les propriétés ACID :

  • Atomicité : Une transaction s’effectue entièrement, ou pas du tout.

  • Cohérence : Le contenu d’une base doit être cohérent au début et à la fin d’une transaction.

  • Isolation : Les modifications d’une transaction ne sont visibles/modifiables que quand celle-ci a été validée.

  • Durabilité : Une fois la transaction validée, l’état de la base est permanent (non affecté par les pannes ou autre).

Le respect de ces propriétés est synonyme de fiabilité dans le traitement de vos données.

Implémenter des transactions peut vite se révéler être une tâche complexe, bien que possible grâce à l’API JDBC.

Encore une fois, Spring Boot se révèle une aide de grande valeur, en évitant toutes les manipulations complexes grâce à l’annotation @Transactional.

Cette annotation peut être appliquée à l’échelle d’une classe ou d’une méthode. Si cette dernière est utilisée, alors Spring utilisera des transactions pour l’exécution des requêtes SQL, garantissant ainsi pour vous le respect des propriétés ACID.

Sachez également que l’utilisation du LAZY au niveau de l’attribut products dans l’entité Category nous oblige, avec Spring Data JPA, à l’utilisation des transactions dans ce contexte.

Pourquoi ?

Lors de l’utilisation du LAZY, l’entité Category est récupérée et son attribut products est substitué par un proxy (un proxy est une classe qui substitue une autre classe). C’est ainsi qu’on obtient le gain de performances, car un proxy est bien évidemment beaucoup plus léger à charger que l’objet réel.

Cependant, lorsqu’on voudra accéder à l’attribut products dans le code, alors le proxy devra laisser place aux vraies données. Et ces données sont accessibles uniquement si nous sommes dans le contexte de la transaction. Dès que nous sortons de la transaction, impossible de substituer un proxy par l’objet correspondant.

Conclusion, l’utilisation du LAZY requiert la mise en place de transactions.

Le code de cette partie de cours est disponible sur le repository du cours à la branche p2c3.

À vous de jouer !

À titre d’exercice, je vous propose de supprimer la relation @ManyToMany que nous avons implémentée côté Category, et de l’implémenter côté Product.

Je vous laisse vous y essayer, et je suis convaincu de votre réussite !

Pour la correction, vous pouvez regarder le code du repository à la branche p2c3_2.

En résumé

  • Les relations sont une composante importante du framework ORM et répondent à des besoins métiers clés.

  • Il existe plusieurs types de relations, comme @OneToMany, @OneToOne ou @ManyToMany

  • Chaque relation peut être :

    • unidirectionnelle : une entité A référence une entité B.

    • bidirectionnelle : une entité A référence une entité B, et l’entité B référence aussi l’entité A.

  • Les transactions sont des blocs de requêtes à exécuter, et ont une très forte valeur ajoutée garantissant le respect des propriétés ACID.

Exemple de certificat de réussite
Exemple de certificat de réussite