Aller au contenu principal

Relations entre entités

Notions théoriques

ManyToOne : plusieurs articles pour un auteur

La relation ManyToOne se place du côté "plusieurs" (l'entité qui contient la clé étrangère). Un article appartient à un auteur : on place donc @ManyToOne dans Article.

// Dans l'entité Article (le côté "plusieurs" de la relation)
@ManyToOne
@JoinColumn(name = "auteur_id") // nom de la colonne de clé étrangère en base
private Utilisateur auteur;
Comparaison avec Doctrine
Doctrine (Symfony)JPA (Spring Boot)
#[ORM\ManyToOne(targetEntity: Utilisateur::class)]@ManyToOne
#[ORM\JoinColumn(name: 'auteur_id')]@JoinColumn(name = "auteur_id")
#[ORM\OneToMany(mappedBy: 'auteur')]@OneToMany(mappedBy = "auteur")
#[ORM\ManyToMany]@ManyToMany

OneToMany : un auteur a plusieurs articles

Du côté "un" (Utilisateur), on déclare @OneToMany. L'attribut mappedBy indique l'attribut Java de l'entité inverse (Article.auteur) qui porte la clé étrangère :

// Dans l'entité Utilisateur (le côté "un" de la relation)
@OneToMany(mappedBy = "auteur", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Article> articles = new ArrayList<>();
  • mappedBy = "auteur" : désigne l'attribut auteur dans Article qui gère la clé étrangère. JPA n'essaie pas de créer une deuxième table de jointure.
  • cascade = CascadeType.ALL : les opérations (persist, merge, remove...) se propagent aux articles de l'utilisateur.
  • orphanRemoval = true : si un article est retiré de la liste articles, il est aussi supprimé de la base.

FetchType : chargement immédiat vs différé

@ManyToOne(fetch = FetchType.LAZY) // Charge l'auteur uniquement si on y accède
private Utilisateur auteur;

@OneToMany(fetch = FetchType.EAGER) // Charge tous les articles en même temps que l'utilisateur
private List<Article> articles;
FetchTypeComportementPar défaut pour
LAZYCharge l'entité liée uniquement quand on y accède@OneToMany, @ManyToMany
EAGERCharge l'entité liée immédiatement avec la requête principale@ManyToOne, @OneToOne
Problème N+1 avec LAZY

Avec FetchType.LAZY sur @ManyToOne, si vous affichez une liste de 100 articles et accédez à article.getAuteur() pour chacun, JPA exécute 1 requête pour la liste + 100 requêtes pour les auteurs = 101 requêtes ! Pour éviter cela, utilisez @EntityGraph ou une requête @Query avec JOIN FETCH.

@EntityGraph pour résoudre le problème N+1

// Dans le repository
@EntityGraph(attributePaths = {"auteur"})
@Override
List<Article> findAll();

// Ou avec une query nommée
@EntityGraph(attributePaths = {"auteur"})
List<Article> findAllByOrderByDateCreationDesc();

Cela génère une seule requête SQL avec JOIN entre articles et utilisateurs.

ManyToMany : articles et tags

// Dans Article
@ManyToMany
@JoinTable(
name = "articles_tags", // Nom de la table de jointure
joinColumns = @JoinColumn(name = "article_id"), // Colonne pour Article
inverseJoinColumns = @JoinColumn(name = "tag_id") // Colonne pour Tag
)
private List<Tag> tags = new ArrayList<>();

// Dans Tag (côté inverse)
@ManyToMany(mappedBy = "tags")
private List<Article> articles = new ArrayList<>();

La table SQL articles_tags sera créée automatiquement avec deux colonnes (article_id, tag_id).

Exemple pratique

Entités Article et Utilisateur reliées :

// Entité Utilisateur
@Entity
@Table(name = "utilisateurs")
public class Utilisateur {

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

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String password;

@OneToMany(mappedBy = "auteur", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Article> articles = new ArrayList<>();

// getters/setters...
}

// Entité Article (extrait)
@Entity
@Table(name = "articles")
public class Article {

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

@NotBlank
private String titre;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "auteur_id", nullable = false)
private Utilisateur auteur;

// getters/setters...
}

Utilisation dans le contrôleur :

// Associer un auteur à un article lors de la création
@PostMapping("/articles/nouveau")
public String creer(@ModelAttribute Article article,
@AuthenticationPrincipal UserDetails userDetails) {
Utilisateur auteur = utilisateurRepository.findByEmail(userDetails.getUsername())
.orElseThrow();
article.setAuteur(auteur);
articleRepository.save(article);
return "redirect:/articles";
}

Test de mémorisation/compréhension


De quel côté de la relation place-t-on @ManyToOne ?


Que signifie mappedBy = "auteur" dans @OneToMany ?


Quel est le FetchType par défaut pour @ManyToOne ?


Qu'est-ce que le problème N+1 ?


Que crée JPA pour une relation @ManyToMany ?


Que fait orphanRemoval = true dans @OneToMany ?


TP pour réfléchir et résoudre des problèmes

Dans ce TP, vous allez lier l'entité Article à l'entité Utilisateur dans le projet MonBlog.

Étape 1 — Ajouter la relation ManyToOne dans Article

Ajoutez l'attribut auteur de type Utilisateur dans la classe Article.


Bonne pratique - Préférer LAZY pour les ManyToOne

Bien que FetchType.EAGER soit la valeur par défaut pour @ManyToOne, il vaut mieux spécifier explicitement FetchType.LAZY pour les relations vers des entités volumineuses ou peu utilisées. Cela évite de charger des données inutiles à chaque requête et peut significativement améliorer les performances.

Étape 2 — Ajouter la relation inverse OneToMany dans Utilisateur


Bonne pratique - Initialiser les collections avec new ArrayList

Initialisez toujours les collections @OneToMany et @ManyToMany avec = new ArrayList<>(). Cela évite les NullPointerException quand on accède à la liste avant le premier chargement, et respecte le contrat Java de ne jamais retourner null pour une collection.

Étape 3 — Mettre à jour le script de migration SQL

Ajoutez la colonne auteur_id à la table articles dans un nouveau script Flyway.


Bonne pratique - Contraintes FK dans les migrations Flyway

Définissez toujours les contraintes de clé étrangère (FOREIGN KEY ... REFERENCES) dans vos scripts SQL Flyway. Cela garantit l'intégrité référentielle au niveau de la base de données, en plus de la validation Java. Si un bug contourne le code Java, la base refusera quand même d'enregistrer un article sans auteur valide.

📌 Une solution