Chapitre 2: Relations entre Entités
Introduction
Dans le monde réel, les données sont liées:
- Un Client passe plusieurs Commandes
- Un Département contient plusieurs Employés
- Un Étudiant suit plusieurs Cours
Ce chapitre vous apprend à modéliser ces relations avec JPA!
Ce que vous allez apprendre
- Les 3 types de relations (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
- Fetch types (LAZY vs EAGER)
- Cascade operations
- Relations bidirectionnelles
Durée: 1h30
Problème Sans Relations
Approche Naïve (Mauvaise)
@Entity
public class Employee {
@Id
private Long id;
private String name;
private Long departmentId; // ❌ Juste un ID!
}
@Entity
public class Department {
@Id
private Long id;
private String name;
}
Problèmes:
// Pour récupérer le département d'un employé:
Employee emp = empRepo.findById(1L);
Department dept = deptRepo.findById(emp.getDepartmentId()); // 2 requêtes!
// Pour récupérer tous les employés d'un département:
List<Employee> emps = empRepo.findByDepartmentId(deptId); // Pas de relation!
Solution: Relations JPA
Avec Relations (Bon)
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne // ✅ Relation JPA!
private Department department;
}
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department") // ✅ Relation inverse!
private List<Employee> employees;
}
Avantages:
// Département d'un employé - FACILE!
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment(); // Accès direct!
// Employés d'un département - FACILE!
Department dept = deptRepo.findById(1L);
List<Employee> emps = dept.getEmployees(); // Accès direct!
Type 1: @OneToOne (1-1)
Qu'est-ce que c'est?
Une entité liée à UNE SEULE autre entité.
Exemples:
- User ↔ UserProfile (un utilisateur a un profil)
- Person ↔ Passport (une personne a un passeport)
- Employee ↔ ParkingSpot (un employé a une place de parking)
Code: User et UserProfile
Entité User:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// Relation OneToOne
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // Clé étrangère dans la table users
private UserProfile profile;
// Constructeurs, getters, setters
}
Entité UserProfile:
@Entity
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
private String avatar;
// Relation inverse (optionnelle)
@OneToOne(mappedBy = "profile")
private User user;
// Constructeurs, getters, setters
}
Utilisation
// Créer
User user = new User("john", "john@email.com");
UserProfile profile = new UserProfile("Bio de John", "avatar.jpg");
user.setProfile(profile);
userRepo.save(user); // Profile sauvegardé automatiquement (cascade)
// Lire
User user = userRepo.findById(1L);
UserProfile profile = user.getProfile();
System.out.println(profile.getBio());
Type 2: @ManyToOne et @OneToMany (N-1 / 1-N)
Qu'est-ce que c'est?
Plusieurs entités liées à UNE entité.
Exemples:
- Department ↔ Employees (un département a plusieurs employés)
- Author ↔ Books (un auteur écrit plusieurs livres)
- Category ↔ Products (une catégorie contient plusieurs produits)
Code: Department et Employee
Côté Many (Employee):
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
// MANY employees belong to ONE department
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // FK dans table employee
private Department department;
// Constructeurs
public Employee() {}
public Employee(String name, Double salary) {
this.name = name;
this.salary = salary;
}
// Getters, setters
}
Côté One (Department):
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// ONE department has MANY employees
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// Constructeurs
public Department() {}
public Department(String name) {
this.name = name;
}
// Helper methods
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Lien bidirectionnel
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
// Getters, setters
}
Utilisation
// Créer un département avec employés
Department it = new Department("IT");
it.addEmployee(new Employee("Alice", 50000.0));
it.addEmployee(new Employee("Bob", 60000.0));
deptRepo.save(it); // Employés sauvegardés automatiquement
// Lire les employés d'un département
Department dept = deptRepo.findById(1L);
List<Employee> employees = dept.getEmployees();
employees.forEach(emp -> System.out.println(emp.getName()));
// Lire le département d'un employé
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment();
System.out.println(dept.getName());
Type 3: @ManyToMany (N-N)
Qu'est-ce que c'est?
Plusieurs entités liées à PLUSIEURS entités.
Exemples:
- Students ↔ Courses (un étudiant suit plusieurs cours, un cours a plusieurs étudiants)
- Books ↔ Authors (un livre a plusieurs auteurs, un auteur écrit plusieurs livres)
- Products ↔ Tags (un produit a plusieurs tags, un tag sur plusieurs produits)
Nécessite une table de jointure!
Code: Student et Course
Entité Student:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// MANY students follow MANY courses
@ManyToMany
@JoinTable(
name = "student_course", // Table de jointure
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
// Helper methods
public void enrollCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// Constructeurs, getters, setters
}
Entité Course:
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Relation inverse
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
// Constructeurs, getters, setters
}
Table générée:
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);
Utilisation
// Créer
Student alice = new Student("Alice");
Course java = new Course("Java Programming");
Course spring = new Course("Spring Boot");
alice.enrollCourse(java);
alice.enrollCourse(spring);
studentRepo.save(alice);
// Lire les cours d'un étudiant
Student student = studentRepo.findById(1L);
List<Course> courses = student.getCourses();
// Lire les étudiants d'un cours
Course course = courseRepo.findById(1L);
List<Student> students = course.getStudents();
Fetch Types: LAZY vs EAGER
Le Problème
Quand charger les entités liées?
Employee emp = empRepo.findById(1L);
// Question: Le Department est-il chargé maintenant?
EAGER (Chargement Immédiat)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
Comportement:
Employee emp = empRepo.findById(1L);
// Department est DÉJÀ chargé!
Department dept = emp.getDepartment(); // Pas de requête supplémentaire
SQL généré:
SELECT e.*, d.*
FROM employee e
LEFT JOIN department d ON e.department_id = d.id
WHERE e.id = 1;
Avantages:
- ✅ Données immédiatement disponibles
- ✅ Pas de requête supplémentaire
Inconvénients:
- ❌ Charge des données même si non utilisées
- ❌ Peut être lent si beaucoup de relations
LAZY (Chargement Différé) - Recommandé
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
Comportement:
Employee emp = empRepo.findById(1L);
// Department PAS encore chargé (proxy)
Department dept = emp.getDepartment(); // Requête MAINTENANT
SQL généré:
-- 1ère requête
SELECT * FROM employee WHERE id = 1;
-- 2ème requête (seulement si on accède à department)
SELECT * FROM department WHERE id = ?;
Avantages:
- ✅ Charge seulement si nécessaire
- ✅ Meilleure performance générale
Inconvénients:
- ⚠️ Requête supplémentaire si on accède
- ⚠️ Peut causer le problème N+1
Quand Utiliser Quoi?
// ✅ LAZY pour @OneToMany (liste)
@OneToMany(fetch = FetchType.LAZY) // Défaut
private List<Employee> employees;
// ✅ LAZY pour @ManyToMany (liste)
@ManyToMany(fetch = FetchType.LAZY) // Défaut
private List<Course> courses;
// ⚠️ LAZY pour @ManyToOne (recommandé)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ⚠️ EAGER seulement si TOUJOURS utilisé
@ManyToOne(fetch = FetchType.EAGER)
private Department department; // Seulement si toujours nécessaire
Règle d'or: Utilisez LAZY par défaut!
Cascade Operations
Le Problème
Que se passe-t-il quand je sauvegarde/supprime une entité avec des relations?
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// Question: L'employé est-il sauvegardé aussi?
Types de Cascade
public enum CascadeType {
PERSIST, // Sauvegarde en cascade
MERGE, // Mise à jour en cascade
REMOVE, // Suppression en cascade
REFRESH, // Rafraîchissement en cascade
DETACH, // Détachement en cascade
ALL // Toutes les opérations
}
Cascade.PERSIST (Sauvegarder)
@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST)
private List<Employee> employees;
Comportement:
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// ✅ Employee sauvegardé automatiquement!
Cascade.REMOVE (Supprimer)
@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE)
private List<Employee> employees;
Comportement:
deptRepo.deleteById(1L);
// ⚠️ TOUS les employés du département sont supprimés!
Attention: Utilisez avec précaution!
Cascade.ALL (Tout)
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;
Comportement:
// Sauvegarde, mise à jour, suppression en cascade
deptRepo.save(dept); // Employés sauvegardés
deptRepo.delete(dept); // Employés supprimés
Recommandations
// ✅ BON - Parent owns children
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // OrderItems appartiennent à Order
// ⚠️ ATTENTION - Shared entities
@ManyToOne(cascade = CascadeType.PERSIST)
private Department department; // PAS de REMOVE!
// ❌ DANGEREUX
@ManyToMany(cascade = CascadeType.REMOVE)
private List<Course> courses; // Supprimerait les cours!
orphanRemoval
Qu'est-ce que c'est?
Supprimer automatiquement les entités qui ne sont plus référencées.
@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;
Comportement:
Department dept = deptRepo.findById(1L);
dept.getEmployees().remove(0); // Retire de la liste
deptRepo.save(dept);
// ✅ L'employé est SUPPRIMÉ de la BD!
Sans orphanRemoval:
// ❌ L'employé reste dans la BD avec department_id = NULL
Relations Bidirectionnelles
Qu'est-ce que c'est?
Les deux entités se connaissent mutuellement.
// Employee connaît son Department
@ManyToOne
private Department department;
// Department connaît ses Employees
@OneToMany(mappedBy = "department")
private List<Employee> employees;
Synchroniser les Deux Côtés
Méthodes helper recommandées:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
// ✅ Helper pour synchroniser
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Synchronise!
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null); // Synchronise!
}
}
Utilisation:
Department dept = new Department("IT");
Employee emp = new Employee("Alice");
// ✅ BON - Utilise le helper
dept.addEmployee(emp);
// ❌ MAUVAIS - Oubli de synchroniser
dept.getEmployees().add(emp); // Oubli de emp.setDepartment(dept)!
Résumé du Chapitre
Les 3 Types de Relations
| Type | Annotation | Exemple | Table Jointure? |
|---|---|---|---|
| 1-1 | @OneToOne | User ↔ Profile | Non |
| N-1 | @ManyToOne | Employee → Department | Non |
| 1-N | @OneToMany | Department → Employees | Non |
| N-N | @ManyToMany | Student ↔ Course | Oui |
Fetch Types
EAGER: Charge immédiatement (JOIN)
LAZY: Charge à la demande (recommandé)
Cascade
PERSIST: Sauvegarde en cascade
REMOVE: Suppression en cascade
ALL: Toutes les opérations
Auto-Évaluation
- Quelle annotation pour "un département a plusieurs employés"?
- Quelle est la différence entre LAZY et EAGER?
- Que fait
orphanRemoval = true? - Pourquoi synchroniser les relations bidirectionnelles?
Réponses:
@OneToManysur Department,@ManyToOnesur Employee- LAZY charge à la demande, EAGER charge immédiatement
- Supprime les entités retirées de la collection
- Pour que les deux côtés de la relation restent cohérents
Prochaine Étape
Vous maîtrisez les relations! Passez aux Requêtes Personnalisées avec @Query.