Aller au contenu principal

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​

  1. Les 3 types de relations (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
  2. Fetch types (LAZY vs EAGER)
  3. Cascade operations
  4. 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​

TypeAnnotationExempleTable Jointure?
1-1@OneToOneUser ↔ ProfileNon
N-1@ManyToOneEmployee → DepartmentNon
1-N@OneToManyDepartment → EmployeesNon
N-N@ManyToManyStudent ↔ CourseOui

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​

  1. Quelle annotation pour "un département a plusieurs employés"?
  2. Quelle est la différence entre LAZY et EAGER?
  3. Que fait orphanRemoval = true?
  4. Pourquoi synchroniser les relations bidirectionnelles?

Réponses:

  1. @OneToMany sur Department, @ManyToOne sur Employee
  2. LAZY charge à la demande, EAGER charge immédiatement
  3. Supprime les entités retirées de la collection
  4. 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.

➡️ Chapitre 3: @Query

⬅️ Chapitre précédent: Conventions

⬅️ Retour au Module 6