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;
| 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'attributauteurdansArticlequi 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 listearticles, 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;
| FetchType | Comportement | Par défaut pour |
|---|---|---|
LAZY | Charge l'entité liée uniquement quand on y accède | @OneToMany, @ManyToMany |
EAGER | Charge l'entité liée immédiatement avec la requête principale | @ManyToOne, @OneToOne |
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
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.
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
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.
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.