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.

Et si vous obteniez un diplĂŽme OpenClassrooms ?
  • Formations jusqu’à 100 % financĂ©es
  • Date de dĂ©but flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous