Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 71

Relations JPA : Commande et Produit

Recommended resources

Bonjour ! Dans la leçon précédente, vous avez appris à utiliser Postman pour tester rigoureusement chaque endpoint de notre API de gestion de livres. Cette compétence est essentielle et vous servira tout au long de votre carrière de développeur backend.

Jusqu'à présent, notre application ne gérait qu'une seule entité. Or, les applications du monde réel sont bien plus complexes et reposent sur des interactions entre de multiples objets métier. Pensez à un site e-commerce : un client passe plusieurs commandes, et chaque commande contient plusieurs produits.

Dans cette leçon, nous allons franchir une étape décisive en commençant notre projet de synthèse : une API pour une application e-commerce. L'objectif est de vous apprendre à modéliser et implémenter les relations entre différentes entités avec JPA, en nous concentrant sur les relations les plus courantes : @OneToMany et @ManyToOne. C'est le cœur de la modélisation de données dans les applications modernes.

1. Modéliser les relations pour une application E-commerce

Avant d'écrire la moindre ligne de code, un bon développeur modélise ses données. Pour notre API e-commerce, imaginons le processus de commande. Une commande (Order) n'est pas simplement liée à une liste de produits (Product). En effet, la relation elle-même contient des informations cruciales : la quantité de chaque produit commandé, et le prix au moment de l'achat.

Pour gérer cela, nous utilisons un modèle classique avec une entité intermédiaire, souvent appelée "ligne de commande" (OrderDetail).

Observez le diagramme UML suivant. Il représente exactement la structure que nous allons construire.

Ce diagramme de classes illustre les relations entre les entités `OrderEntity`, `ProductEntity` et `OrderDetailEntity`. Une commande (`OrderEntity`) "a" (`has`) plusieurs lignes de commande (`OrderDetailEntity`). Chaque ligne de commande "contient" (`contains`) un produit (`ProductEntity`).

Ce diagramme nous montre que :

  • Une OrderEntity est liée à plusieurs OrderDetailEntity (relation One-to-Many).
  • Une ProductEntity peut apparaître dans plusieurs OrderDetailEntity (relation One-to-Many).
  • Chaque OrderDetailEntity est liée à une seule OrderEntity et à un seul ProductEntity (deux relations Many-to-One).

En décomposant la relation complexe "plusieurs commandes contiennent plusieurs produits" (@ManyToMany) en deux relations plus simples (@OneToMany / @ManyToOne), nous gagnons en clarté et nous pouvons stocker des informations propres à la relation, comme la quantité.


2. La relation bidirectionnelle @OneToMany et @ManyToOne

Concentrons-nous d'abord sur la relation entre OrderEntity et OrderDetailEntity. C'est une relation bidirectionnelle : à partir d'une commande, on veut pouvoir retrouver ses lignes, et à partir d'une ligne, on veut savoir à quelle commande elle appartient.

Pour implémenter cela, JPA a besoin de savoir qui "possède" la relation. La partie propriétaire (l'entité "esclave") est celle qui stocke la clé étrangère dans la base de données. Dans notre cas, la table order_details contiendra une colonne order_id. C'est donc OrderDetailEntity qui est propriétaire de la relation.

  • Côté propriétaire (@ManyToOne) : Dans OrderDetailEntity, on déclare la relation vers OrderEntity.
  • Côté inverse (@OneToMany) : Dans OrderEntity, on déclare la collection de OrderDetailEntity et on signale à JPA que la relation est déjà gérée de l'autre côté.

Voyons le code.

Étape 1 : Le côté propriétaire (OrderDetailEntity)

Dans OrderDetailEntity, nous avons une relation ManyToOne car plusieurs lignes de commande peuvent appartenir à une commande.

@Entity
@Table(name = "order_details")
public class OrderDetailEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int quantity;
    private double unitPrice;

    @ManyToOne
    @JoinColumn(name = "order_id", nullable = false) // Définit la colonne de clé étrangère
    private OrderEntity order;

    @ManyToOne
    @JoinColumn(name = "product_id", nullable = false)
    private ProductEntity product;

    // Getters, setters, etc.
}
  • @ManyToOne : Indique le type de relation.
  • @JoinColumn(name = "order_id") : C'est l'annotation cruciale. Elle indique à JPA que la table order_details doit avoir une colonne nommée order_id qui sera une clé étrangère pointant vers la clé primaire de la table orders.

Étape 2 : Le côté inverse (OrderEntity)

Dans OrderEntity, la relation est OneToMany car une commande peut avoir plusieurs lignes de commande.

@Entity
@Table(name = "orders")
public class OrderEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime orderDate;
    private String customerName; // Simplifié pour l'instant

    @OneToMany(mappedBy = "order") // "order" est le nom de l'attribut dans OrderDetailEntity
    private Set<OrderDetailEntity> orderDetails = new HashSet<>();

    // Getters, setters, etc.
}
  • @OneToMany : Indique la relation "un-à-plusieurs".
  • mappedBy = "order" : C'est l'autre annotation fondamentale. Elle dit à JPA : "Ne crée pas de colonne ou de table de jointure pour cette relation. La manière dont elle est mappée est déjà définie par l'attribut order dans la classe OrderDetailEntity".

L'image suivante illustre parfaitement ce que nous venons de faire :

Cette illustration montre une relation bidirectionnelle entre `EntityA` (notre `OrderEntity`) et `EntityB` (notre `OrderDetailEntity`). Vous pouvez voir `@OneToMany` avec `mappedBy` d'un côté, et `@ManyToOne` avec la clé étrangère de l'autre, ce qui correspond exactement à notre implémentation.

3. Voir les relations en action

La théorie est une chose, mais voir un expert l'implémenter et expliquer ses choix est encore plus parlant. La vidéo suivante illustre parfaitement comment mettre en place les différents types de relations JPA.

Comprendre le mapping JPA avec Spring Boot - OneToOne, OneToMany, ManyToMany & Self Referencing

Dans cette vidéo de "Coding With Armand", l'auteur explique comment mapper les différentes relations JPA dans un projet Spring Boot.

Regardez attentivement la section sur la relation un-à-plusieurs : relation OneToMany. L'exemple (Utilisateur et Post) est très similaire à notre cas (Commande et LigneCommande). Concentrez-vous sur l'utilisation de @ManyToOne et @JoinColumn du côté "plusieurs" et de @OneToMany avec mappedBy du côté "un". Observez comment le schéma de base de données est généré avec une clé étrangère, et non une table de jointure.

Une question que vous pourriez vous poser est : pourquoi utiliser Set<OrderDetailEntity> plutôt que List<OrderDetailEntity> ?

  • Set : Garantit l'unicité des éléments (basé sur les méthodes equals() et hashCode()). C'est souvent le meilleur choix pour les relations gérées par JPA pour éviter les problèmes de duplication et de performance.
  • List : Autorise les doublons et préserve l'ordre d'insertion.

En général, il est préférable de typer vos collections avec des interfaces (Set, List) plutôt qu'avec des implémentations concrètes (HashSet, ArrayList), car JPA peut fournir sa propre implémentation optimisée de la collection.


4. Compléter notre modèle de données

Maintenant que vous avez compris le principe, nous pouvons finaliser notre modèle en créant l'entité ProductEntity et en nous assurant que toutes les relations sont correctement définies.

Voici le code complet des trois entités que vous allez ajouter à votre projet. Créez un nouveau package com.example.ecommerce.entity et placez-y ces classes.

ProductEntity.java

package com.example.ecommerce.entity;

import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "products")
public class ProductEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    private String description;

    @Column(nullable = false)
    private double price;
    
    // Un produit peut être dans plusieurs lignes de commande
    @OneToMany(mappedBy = "product")
    private Set<OrderDetailEntity> orderDetails = new HashSet<>();
    
    // Constructeurs, Getters et Setters...
}

OrderEntity.java

package com.example.ecommerce.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "orders")
public class OrderEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private LocalDateTime orderDate;

    @Column(nullable = false)
    private String customerName;

    // Une commande a plusieurs lignes de commande
    @OneToMany(mappedBy = "order")
    private Set<OrderDetailEntity> orderDetails = new HashSet<>();

    // Constructeurs, Getters et Setters...
}

OrderDetailEntity.java

package com.example.ecommerce.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "order_details")
public class OrderDetailEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private int quantity;

    // Prix au moment de la commande pour figer l'historique
    @Column(nullable = false)
    private double unitPrice;

    // Plusieurs lignes peuvent appartenir à une seule commande
    @ManyToOne(fetch = FetchType.LAZY) // LAZY est une bonne pratique
    @JoinColumn(name = "order_id", nullable = false)
    private OrderEntity order;
    
    // Plusieurs lignes peuvent concerner le même produit
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private ProductEntity product;
    
    // Constructeurs, Getters et Setters...
}

Note sur fetch = FetchType.LAZY:
J'ai ajouté fetch = FetchType.LAZY sur les relations @ManyToOne. C'est une bonne pratique de performance.

  • LAZY (chargement paresseux) : Les données de l'entité liée (ici, OrderEntity et ProductEntity) ne sont chargées de la base de données que lorsque vous y accédez explicitement (par ex. orderDetail.getOrder()). C'est le défaut pour @OneToMany.
  • EAGER (chargement immédiat) : Les données de l'entité liée sont toujours chargées en même temps que l'entité principale. C'est le défaut pour @ManyToOne. Forcer LAZY évite de charger des graphes d'objets entiers inutilement, ce qui prévient le fameux problème "N+1 select". Nous reviendrons sur ces optimisations plus en détail.

Pour un résumé textuel de toutes ces annotations, vous pouvez consulter la ressource suivante.

Les relations avec JPA — Documentation Java ORM / Spring

Cette documentation de Gayerie.dev est une excellente référence sur les relations JPA.

Lisez les sections sur "La relation n:1 (many to one)", "La relation 1:n (one to many)" et "Les relations bi-directionnelles". Cela vous permettra de consolider ce que vous venez d'apprendre sur @ManyToOne, @OneToMany et mappedBy.


Conclusion

Cette leçon marque une étape majeure. Vous êtes passé de la gestion d'une entité isolée à la modélisation de relations complexes, qui est le fondement de la plupart des applications professionnelles.

Points clés à retenir :

  • Les relations entre entités (@OneToMany, @ManyToOne, etc.) sont au cœur de JPA.
  • Une relation bidirectionnelle a un côté propriétaire (qui définit la clé étrangère avec @JoinColumn) et un côté inverse (qui utilise mappedBy pour faire référence au propriétaire).
  • Dans une relation @OneToMany/@ManyToOne, le côté @ManyToOne est généralement le côté propriétaire.
  • Une relation @ManyToMany est souvent mieux modélisée avec une entité intermédiaire pour stocker des attributs propres à la relation (ex: quantité).
  • Utiliser des interfaces (Set, List) pour les collections et préférer le chargement LAZY sont des bonnes pratiques de performance.

Dans la prochaine leçon, nous allons rendre ces entités vivantes. Maintenant que le modèle est défini, nous allons créer les interfaces Repository avec Spring Data JPA pour chacune d'elles. Cela nous permettra d'effectuer des opérations CRUD sur notre nouvelle structure de données relationnelle.