framework-web:spring:relations

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentes Révision précédente
Prochaine révision
Révision précédente
framework-web:spring:relations [2025/10/07 14:27] jcheronframework-web:spring:relations [2025/10/07 15:28] (Version actuelle) jcheron
Ligne 1: Ligne 1:
-====== Relation JPA ======+====== Relations JPA ======
  
 ===== Les Types de Relations ===== ===== Les Types de Relations =====
Ligne 7: Ligne 7:
 Cas d'usage : Un auteur a plusieurs livres, un livre a un seul auteur. Cas d'usage : Un auteur a plusieurs livres, un livre a un seul auteur.
  
-=== Configuration Unidirectionnelle (Rarement recommandée) ===+=== Configuration Unidirectionnelle (❌ Rarement recommandée) ===
 <sxh java> <sxh java>
 @Entity @Entity
Ligne 28: Ligne 28:
 } }
 </sxh> </sxh>
-<WRAP round important 60%+<WRAP round important> 
-Problème : Génère des UPDATE supplémentaires !+**Problème** : Génère des UPDATE supplémentaires ! 
 + 
 +Lancés automatiquement par Hibernate pour mettre à jour la clé étrangère.
 </WRAP> </WRAP>
  
  
-=== Configuration Bidirectionnelle (Recommandée) ===+=== Configuration Bidirectionnelle (✅ Recommandée) ===
 <sxh java> <sxh java>
 @Entity @Entity
Ligne 75: Ligne 77:
 } }
 </sxh> </sxh>
-<WRAP round tip 60%+<WRAP round tip> 
-Règle d'or : Le côté @ManyToOne est TOUJOURS le propriétaire (owner) de la relation.+Le côté **@ManyToOne** est TOUJOURS le propriétaire (owner) de la relation. 
 + 
 +Toutes les modifications portant sur cette relation devront se faire côté **owner**. 
 +</WRAP> 
 + 
 + 
 +==== @ManyToMany ==== 
 + 
 +Cas d'usage Un étudiant suit plusieurs cours, un cours a plusieurs étudiants. 
 + 
 +=== Configuration Unidirectionnelle === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Student { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id; 
 +     
 +    @ManyToMany 
 +    @JoinTable( 
 +        name = "student_course", 
 +        joinColumns = @JoinColumn(name = "student_id"), 
 +        inverseJoinColumns = @JoinColumn(name = "course_id"
 +    ) 
 +    private Set<Course> courses = new HashSet<>(); 
 +
 + 
 +@Entity 
 +public class Course { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id; 
 +    // Pas de référence à Student 
 +
 +</sxh> 
 + 
 +=== Configuration Bidirectionnelle (✅ Recommandée) === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Student { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id; 
 +     
 +    @ManyToMany( 
 +        cascade = {CascadeType.PERSIST, CascadeType.MERGE}, 
 +        fetch = FetchType.LAZY 
 +    ) 
 +    @JoinTable( 
 +        name = "student_course", 
 +        joinColumns = @JoinColumn(name = "student_id"), 
 +        inverseJoinColumns = @JoinColumn(name = "course_id"
 +    ) 
 +    private Set<Course> courses = new HashSet<>(); 
 +     
 +    public void enrollCourse(Course course) { 
 +        courses.add(course); 
 +        course.getStudents().add(this); 
 +    } 
 +     
 +    public void unenrollCourse(Course course) { 
 +        courses.remove(course); 
 +        course.getStudents().remove(this); 
 +    } 
 +
 + 
 +@Entity 
 +public class Course { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id; 
 +     
 +    @ManyToMany(mappedBy = "courses"
 +    private Set<Student> students = new HashSet<>(); 
 +
 +</sxh> 
 + 
 +==== ManyToMany avec attributs ==== 
 +Ce n'est pas vraiment une ManyToMany... 
 + 
 +=== Avec @EmbeddedId === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Enrollment { 
 +    @EmbeddedId 
 +    private EnrollmentId id; 
 +     
 +    @ManyToOne 
 +    @MapsId("studentId"
 +    private Student student; 
 +     
 +    @ManyToOne 
 +    @MapsId("courseId"
 +    private Course course; 
 +     
 +    private LocalDate enrollmentDate; 
 +    private Integer grade; 
 +
 + 
 +@Embeddable 
 +public class EnrollmentId implements Serializable { 
 +    private Long studentId; 
 +    private Long courseId; 
 +     
 +    // equals() et hashCode() 
 +
 + 
 +@Entity 
 +public class Student { 
 +    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true) 
 +    private Set<Enrollment> enrollments = new HashSet<>(); 
 +
 + 
 +@Entity 
 +public class Course { 
 +    @OneToMany(mappedBy = "course", cascade = CascadeType.ALL, orphanRemoval = true) 
 +    private Set<Enrollment> enrollments = new HashSet<>(); 
 +
 +</sxh> 
 + 
 +=== Sans @EmbeddedId, mais avec Id supplémentaire (✅ Recommandée) === 
 + 
 +<sxh java> 
 +@Entity 
 +@Table( 
 +    name = "enrollment", 
 +    indexes = { 
 +        @Index( 
 +            name = "idx_enrollment_pk",  
 +            columnList = "student_id, course_id",  
 +            unique = true 
 +        ) 
 +    } 
 +
 +public class Enrollment { 
 +     
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id;  // ← Clé primaire auto-générée simple 
 +     
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "student_id", nullable = false) 
 +    private Student student; 
 +     
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    @JoinColumn(name = "course_id", nullable = false) 
 +    private Course course; 
 +     
 +    private Integer grade; 
 +     
 +    private LocalDate enrollmentDate; 
 +     
 +    // Constructeurs 
 +     
 +    // Getters/Setters 
 +     
 +    // equals et hashCode 
 +     
 +    @Override 
 +    public boolean equals(Object o) { 
 +        if (this == o) return true; 
 +        if (!(o instanceof Enrollment)) return false; 
 +        Enrollment that = (Enrollment) o; 
 +        return id != null && id.equals(that.getId()); 
 +    } 
 +     
 +    @Override 
 +    public int hashCode() { 
 +        return getClass().hashCode(); 
 +    } 
 +     
 +    @Override 
 +    public String toString() { 
 +        return "Enrollment{"
 +                "id=" + id + 
 +                ", student=" + (student != null ? student.getName() : "null") + 
 +                ", course=" + (course != null ? course.getName() : "null") + 
 +                ", grade=" + grade + 
 +                ", enrollmentDate=" + enrollmentDate + 
 +                '}'; 
 +    } 
 +
 +</sxh> 
 + 
 +<WRAP round tip> 
 +**Avantages de cette approche** : 
 +  * Code simple et lisible 
 +  * Pas de classe @Embeddable à gérer 
 +  * Fonctionne parfaitement avec Spring Data 
 +  * L'index unique garantit l'unicité (student_id, course_id) 
 +  * Performance identique à une PK composite 
 +</WRAP> 
 + 
 + 
 +===== FetchType : LAZY vs EAGER ===== 
 + 
 +==== LAZY (✅ Recommandé par défaut) ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Book { 
 +    @ManyToOne(fetch = FetchType.LAZY) 
 +    private Author author; 
 +
 +</sxh> 
 + 
 +**Comportement** : 
 +  * Les données ne sont chargées **que si on y accède** 
 +  * Évite les requêtes inutiles 
 +  * Peut lancer une ''LazyInitializationException'' si la session Hibernate est fermée 
 + 
 +**Exemple** : 
 +<sxh java> 
 +Book book = bookRepository.findById(1L).get(); 
 +// Pas de requête vers Author ici 
 + 
 +String authorName = book.getAuthor().getName();  
 +// ← Requête SQL lancée ici (si session ouverte) 
 +</sxh> 
 + 
 +<WRAP round important> 
 +**LazyInitializationException** : Se produit quand on accède à une relation LAZY en dehors d'une transaction. 
 + 
 +**Solutions** : 
 +  * Utiliser ''@Transactional'' sur la méthode 
 +  * Utiliser un ''@EntityGraph'' 
 +  * Utiliser une query avec ''JOIN FETCH'' 
 +</WRAP> 
 + 
 +==== EAGER (⚠️ À utiliser avec précaution) ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Book { 
 +    @ManyToOne(fetch = FetchType.EAGER) 
 +    private Author author; 
 +
 +</sxh> 
 + 
 +**Comportement** : 
 +  * Les données sont chargées **immédiatement** avec l'entité principale 
 +  * Peut créer des problèmes de performance (N+1) 
 +  * Utile seulement si on a **toujours** besoin de la relation 
 + 
 +==== FetchType par défaut ==== 
 + 
 +^ Annotation ^ FetchType par défaut ^ 
 +| ''@ManyToOne'' | ''LAZY'' (depuis Hibernate 5.1+) ou ''EAGER'' (JPA standard) | 
 +| ''@OneToOne'' | ''EAGER''
 +| ''@OneToMany'' | ''LAZY''
 +| ''@ManyToMany'' | ''LAZY''
 + 
 +<WRAP round tip> 
 +**Bonne pratique** : Toujours spécifier explicitement le ''FetchType'' pour éviter les surprises. 
 +</WRAP> 
 + 
 +==== Optimiser avec JOIN FETCH ==== 
 + 
 +<sxh java> 
 +public interface BookRepository extends JpaRepository<Book, Long> { 
 +     
 +    // ✅ Charge Book + Author en une seule requête 
 +    @Query("SELECT b FROM Book b JOIN FETCH b.author WHERE b.id = :id"
 +    Optional<Book> findByIdWithAuthor(@Param("id") Long id); 
 +     
 +    // ✅ Charge tous les Books + leurs Authors 
 +    @Query("SELECT DISTINCT b FROM Book b JOIN FETCH b.author"
 +    List<Book> findAllWithAuthors(); 
 +
 +</sxh> 
 + 
 +==== Optimiser avec @EntityGraph ==== 
 + 
 +<sxh java> 
 +public interface BookRepository extends JpaRepository<Book, Long> { 
 +     
 +    @EntityGraph(attributePaths = {"author"}) 
 +    Optional<Book> findById(Long id); 
 +     
 +    @EntityGraph(attributePaths = {"author", "publisher"}) 
 +    List<Book> findAll(); 
 +
 +</sxh> 
 + 
 + 
 +===== Cascade ===== 
 + 
 +Les ''CascadeType'' définissent quelles opérations sont propagées aux entités liées. 
 + 
 +==== Les différents types ==== 
 + 
 +<sxh java> 
 +public enum CascadeType { 
 +    PERSIST,    // entityManager.persist() 
 +    MERGE,      // entityManager.merge() 
 +    REMOVE,     // entityManager.remove() 
 +    REFRESH,    // entityManager.refresh() 
 +    DETACH,     // entityManager.detach() 
 +    ALL         // Tous les types ci-dessus 
 +
 +</sxh> 
 + 
 +==== Exemples ==== 
 + 
 +=== PERSIST : Propager la création === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany( 
 +        mappedBy = "author", 
 +        cascade = CascadeType.PERSIST 
 +    ) 
 +    private List<Book> books = new ArrayList<>(); 
 +
 + 
 +// Utilisation 
 +Author author = new Author("John Doe"); 
 +Book book1 = new Book("Book 1"); 
 +Book book2 = new Book("Book 2"); 
 + 
 +author.addBook(book1); 
 +author.addBook(book2); 
 + 
 +entityManager.persist(author); 
 +// ✅ author, book1 et book2 sont tous persistés automatiquement 
 +</sxh> 
 + 
 +=== REMOVE : Propager la suppression (⚠️ Dangereux) === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany( 
 +        mappedBy = "author", 
 +        cascade = CascadeType.REMOVE  // ⚠️ Attention ! 
 +    ) 
 +    private List<Book> books = new ArrayList<>(); 
 +
 + 
 +// Utilisation 
 +Author author = authorRepository.findById(1L).get(); 
 +authorRepository.delete(author); 
 +// ⚠️ Tous les livres de cet auteur sont aussi supprimés ! 
 +</sxh> 
 + 
 +<WRAP round important> 
 +**''CascadeType.REMOVE'' vs ''orphanRemoval''** : 
 +  * ''REMOVE'' : Supprime les entités liées quand on supprime le parent 
 +  * ''orphanRemoval'' : Supprime les entités liées quand on les retire de la collection 
 + 
 +**Exemple** : 
 +<sxh java> 
 +// Avec CascadeType.REMOVE uniquement 
 +author.getBooks().remove(book);  // ❌ book reste en base avec author_id = NULL 
 + 
 +// Avec orphanRemoval = true 
 +author.getBooks().remove(book);  // ✅ book est supprimé de la base 
 +</sxh> 
 +</WRAP> 
 + 
 +=== ALL : Propager toutes les opérations === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany( 
 +        mappedBy = "author", 
 +        cascade = CascadeType.ALL, 
 +        orphanRemoval = true 
 +    ) 
 +    private List<Book> books = new ArrayList<>(); 
 +
 +</sxh> 
 + 
 +==== Bonnes pratiques Cascade ==== 
 + 
 +<WRAP round tip> 
 +**Recommandations** : 
 +  * **''@OneToMany''** : Utiliser ''CascadeType.ALL'' + ''orphanRemoval = true'' pour les relations de composition (ex: Order → OrderItems) 
 +  * **''@ManyToOne''** : NE PAS utiliser de cascade (sauf cas très particuliers) 
 +  * **''@ManyToMany''** : Utiliser ''CascadeType.PERSIST'' et ''CascadeType.MERGE'' uniquement 
 +  * **''CascadeType.REMOVE''** : Attention aux suppressions en cascade non voulues ! 
 +</WRAP> 
 + 
 + 
 +===== orphanRemoval ===== 
 + 
 +''orphanRemoval = true'' supprime automatiquement les entités "orphelines" (qui ne sont plus dans la collection parente). 
 + 
 +==== Exemple ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany( 
 +        mappedBy = "author", 
 +        cascade = CascadeType.ALL, 
 +        orphanRemoval = true  // ← Suppression automatique des orphelins 
 +    ) 
 +    private List<Book> books = new ArrayList<>(); 
 +     
 +    public void removeBook(Book book) { 
 +        books.remove(book); 
 +        book.setAuthor(null); 
 +    } 
 +
 + 
 +// Utilisation 
 +Author author = authorRepository.findById(1L).get(); 
 +Book book = author.getBooks().get(0); 
 + 
 +author.removeBook(book); 
 +authorRepository.save(author); 
 +// ✅ book est automatiquement supprimé de la base 
 +</sxh> 
 + 
 +==== Différence avec CascadeType.REMOVE ==== 
 + 
 +^ Opération ^ orphanRemoval ^ CascadeType.REMOVE ^ 
 +| Supprimer le parent | ✅ Supprime les enfants | ✅ Supprime les enfants | 
 +| Retirer un enfant de la collection | ✅ Supprime l'enfant | ❌ L'enfant reste (FK = NULL) | 
 + 
 +<WRAP round tip> 
 +**orphanRemoval = true** est idéal pour les relations de **composition** (parent-enfant fort) où l'enfant n'a pas de sens sans le parent. 
 + 
 +**Exemples** : Order → OrderItem, Invoice → InvoiceLine, Blog → Comment 
 +</WRAP> 
 + 
 + 
 +===== Incidence du Owner (propriétaire) de la Relation ===== 
 + 
 +==== Définition ==== 
 + 
 +Le **owner** (propriétaire) de la relation est le côté qui : 
 +  * Possède la colonne de clé étrangère (FK) en base de données 
 +  * Est responsable de la synchronisation avec la base de données 
 +  * **N'a PAS** l'attribut ''mappedBy'' 
 + 
 +<WRAP round important> 
 +**Règle absolue** : Pour qu'une modification de relation soit persistée, elle DOIT être faite côté owner. 
 +</WRAP> 
 + 
 +==== Schéma de base de données ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author" // ❌ N'est PAS le owner 
 +    private List<Book> books; 
 +
 + 
 +@Entity 
 +public class Book { 
 +    @ManyToOne 
 +    @JoinColumn(name = "author_id" // ✅ EST le owner 
 +    private Author author; 
 +
 +</sxh> 
 + 
 +**En base de données** : 
 +<code sql> 
 +CREATE TABLE author ( 
 +    id BIGINT PRIMARY KEY, 
 +    name VARCHAR(255) 
 +); 
 + 
 +CREATE TABLE book ( 
 +    id BIGINT PRIMARY KEY, 
 +    title VARCHAR(255), 
 +    author_id BIGINT,  -- ← LA CLÉ ÉTRANGÈRE EST ICI ! 
 +    CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES author(id) 
 +); 
 +</code> 
 + 
 +==== Conséquence : Seul le Owner peut persister la relation ==== 
 + 
 +=== ❌ Ce qui NE fonctionne PAS === 
 + 
 +<sxh java> 
 +Author author = new Author("John Doe"); 
 +Book book = new Book("My Book"); 
 + 
 +// ❌ Modifier uniquement le côté inverse (non-owner) 
 +author.getBooks().add(book); 
 +entityManager.persist(author); 
 +entityManager.flush(); 
 + 
 +// Résultat en base : 
 +// book.author_id = NULL ❌ 
 +// La FK n'est PAS remplie ! 
 +</sxh> 
 + 
 +**Pourquoi ?** Hibernate ignore le côté ''@OneToMany'' sans ''mappedBy'' pour la persistance. 
 + 
 +=== ✅ La bonne méthode === 
 + 
 +<sxh java> 
 +Author author = new Author("John Doe"); 
 +Book book = new Book("My Book"); 
 + 
 +// ✅ Modifier le côté owner (@ManyToOne) 
 +book.setAuthor(author); 
 + 
 +entityManager.persist(author); 
 +entityManager.persist(book); 
 +entityManager.flush(); 
 + 
 +// Résultat en base : 
 +// book.author_id = 1 ✅ 
 +</sxh> 
 + 
 +=== ✅ La méthode recommandée : Méthodes helper === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true) 
 +    private List<Book> books = new ArrayList<>(); 
 +     
 +    // ✅ Méthode qui synchronise les deux côtés 
 +    public void addBook(Book book) { 
 +        books.add(book);       // Côté inverse (pour cohérence en mémoire) 
 +        book.setAuthor(this);  // Côté owner (pour persistance) 
 +    } 
 +     
 +    public void removeBook(Book book) { 
 +        books.remove(book); 
 +        book.setAuthor(null); 
 +    } 
 +
 + 
 +// Utilisation 
 +Author author = new Author("John Doe"); 
 +Book book = new Book("My Book"); 
 + 
 +author.addBook(book);  // ✅ Les deux côtés sont synchronisés 
 + 
 +authorRepository.save(author);  // ✅ Tout est persisté correctement 
 +</sxh> 
 + 
 +==== Performance : Unidirectionnel vs Bidirectionnel ==== 
 + 
 +=== Configuration Unidirectionnelle @OneToMany (❌ Mauvaise perf) === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany 
 +    @JoinColumn(name = "author_id"
 +    private List<Book> books = new ArrayList<>(); 
 +
 +</sxh> 
 + 
 +**Problème** : Génère des requêtes supplémentaires ! 
 + 
 +<code sql> 
 +INSERT INTO author (name) VALUES ('John Doe'); 
 +INSERT INTO book (title) VALUES ('Book 1'); 
 +INSERT INTO book (title) VALUES ('Book 2'); 
 + 
 +-- ⚠️ UPDATE supplémentaires pour chaque livre ! 
 +UPDATE book SET author_id = 1 WHERE id = 1; 
 +UPDATE book SET author_id = 1 WHERE id = 2; 
 +</code> 
 + 
 +=== Configuration Bidirectionnelle (✅ Meilleure perf) === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL) 
 +    private List<Book> books = new ArrayList<>(); 
 +
 + 
 +@Entity 
 +public class Book { 
 +    @ManyToOne 
 +    @JoinColumn(name = "author_id"
 +    private Author author; 
 +
 +</sxh> 
 + 
 +**Requêtes optimisées** : 
 + 
 +<code sql> 
 +INSERT INTO author (name) VALUES ('John Doe'); 
 +INSERT INTO book (title, author_id) VALUES ('Book 1', 1);  -- ✅ FK directement 
 +INSERT INTO book (title, author_id) VALUES ('Book 2', 1);  -- ✅ Pas d'UPDATE 
 +</code> 
 + 
 +==== ManyToMany : Qui est le owner ? ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Student { 
 +    @ManyToMany  // ✅ Le côté SANS mappedBy est le owner 
 +    @JoinTable( 
 +        name = "student_course", 
 +        joinColumns = @JoinColumn(name = "student_id"), 
 +        inverseJoinColumns = @JoinColumn(name = "course_id"
 +    ) 
 +    private Set<Course> courses = new HashSet<>(); 
 +
 + 
 +@Entity 
 +public class Course { 
 +    @ManyToMany(mappedBy = "courses" // ❌ Côté inverse 
 +    private Set<Student> students = new HashSet<>(); 
 +
 +</sxh> 
 + 
 +**Conséquence** : Pour ajouter une relation, il faut modifier ''Student.courses'' (le owner) : 
 + 
 +<sxh java> 
 +// ✅ Modifier le owner 
 +student.getCourses().add(course); 
 +studentRepository.save(student); 
 + 
 +// ❌ Modifier l'inverse (ignoré par Hibernate) 
 +course.getStudents().add(student); 
 +courseRepository.save(course);  // Ne persiste PAS la relation ! 
 +</sxh> 
 + 
 +<WRAP round tip> 
 +**Toujours synchroniser les deux côtés** avec des méthodes helper : 
 +<sxh java> 
 +public void enrollCourse(Course course) { 
 +    courses.add(course);             // Owner 
 +    course.getStudents().add(this);  // Inverse 
 +
 +</sxh> 
 +</WRAP> 
 + 
 + 
 +===== Queries et Problèmes Courants ===== 
 + 
 +==== Problème N+1 ==== 
 + 
 +Le problème N+1 se produit quand on charge une collection d'entités, puis qu'on accède à une relation, déclenchant **une requête SQL par entité**. 
 + 
 +=== Exemple du problème === 
 + 
 +<sxh java> 
 +// 1 requête pour charger tous les livres 
 +List<Book> books = bookRepository.findAll(); 
 + 
 +// N requêtes supplémentaires (une par livre) pour charger les auteurs ! 
 +for (Book book : books) { 
 +    System.out.println(book.getAuthor().getName());  // ⚠️ 1 requête SQL 
 +
 + 
 +// Total : 1 + N requêtes ! 
 +</sxh> 
 + 
 +<code sql> 
 +SELECT * FROM book;                    -- Requête 1 
 +SELECT * FROM author WHERE id = 1;     -- Requête 2 
 +SELECT * FROM author WHERE id = 2;     -- Requête 3 
 +SELECT * FROM author WHERE id = 3;     -- Requête 4 
 +... 
 +</code> 
 + 
 +=== Solution 1 : JOIN FETCH === 
 + 
 +<sxh java> 
 +public interface BookRepository extends JpaRepository<Book, Long> { 
 +     
 +    @Query("SELECT b FROM Book b JOIN FETCH b.author"
 +    List<Book> findAllWithAuthors(); 
 +
 + 
 +// Utilisation 
 +List<Book> books = bookRepository.findAllWithAuthors(); 
 +// ✅ 1 seule requête SQL avec JOIN ! 
 + 
 +for (Book book : books) { 
 +    System.out.println(book.getAuthor().getName());  // ✅ Pas de requête 
 +
 +</sxh> 
 + 
 +<code sql> 
 +-- ✅ Une seule requête ! 
 +SELECT b.*, a.*  
 +FROM book b  
 +INNER JOIN author a ON b.author_id = a.id; 
 +</code> 
 + 
 +=== Solution 2 : @EntityGraph === 
 + 
 +<sxh java> 
 +public interface BookRepository extends JpaRepository<Book, Long> { 
 +     
 +    @EntityGraph(attributePaths = {"author"}) 
 +    List<Book> findAll(); 
 +     
 +    @EntityGraph(attributePaths = {"author", "publisher"}) 
 +    Optional<Book> findById(Long id); 
 +
 +</sxh> 
 + 
 +=== Solution 3 : @Fetch(FetchMode.SUBSELECT) === 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author"
 +    @Fetch(FetchMode.SUBSELECT)  // ✅ 2 requêtes au lieu de N+1 
 +    private List<Book> books = new ArrayList<>(); 
 +
 +</sxh> 
 + 
 +<code sql> 
 +SELECT * FROM author;  -- Requête 1 
 + 
 +-- Requête 2 : charge tous les livres en une fois 
 +SELECT * FROM book WHERE author_id IN ( 
 +    SELECT id FROM author 
 +); 
 +</code> 
 + 
 +==== LazyInitializationException ==== 
 + 
 +**Erreur** : ''org.hibernate.LazyInitializationException: could not initialize proxy - no Session'' 
 + 
 +**Cause** : Accès à une relation LAZY en dehors d'une transaction/session Hibernate. 
 + 
 +=== Exemple du problème === 
 + 
 +<sxh java> 
 +@Service 
 +public class BookService { 
 +     
 +    public Book getBook(Long id) { 
 +        return bookRepository.findById(id).get(); 
 +    }  // ← La transaction se termine ici 
 +
 + 
 +// Controller 
 +Book book = bookService.getBook(1L); 
 +String authorName = book.getAuthor().getName();   
 +// ❌ LazyInitializationException ! 
 +</sxh> 
 + 
 +=== Solution 1 : @Transactional === 
 + 
 +<sxh java> 
 +@Service 
 +public class BookService { 
 +     
 +    @Transactional(readOnly = true) 
 +    public Book getBook(Long id) { 
 +        Book book = bookRepository.findById(id).get(); 
 +        // Accéder aux relations LAZY ici 
 +        book.getAuthor().getName();  // ✅ OK, on est dans la transaction 
 +        return book; 
 +    } 
 +
 +</sxh> 
 + 
 +=== Solution 2 : JOIN FETCH === 
 + 
 +<sxh java> 
 +@Query("SELECT b FROM Book b JOIN FETCH b.author WHERE b.id = :id"
 +Optional<Book> findByIdWithAuthor(@Param("id") Long id); 
 +</sxh> 
 + 
 +=== Solution 3 : @EntityGraph === 
 + 
 +<sxh java> 
 +@EntityGraph(attributePaths = {"author"}) 
 +Optional<Book> findById(Long id); 
 +</sxh> 
 + 
 +=== Solution 4 : DTO Projection === 
 + 
 +<sxh java> 
 +public interface BookDTO { 
 +    Long getId(); 
 +    String getTitle(); 
 +    String getAuthorName();  // author.name 
 +
 + 
 +@Query("SELECT b.id as id, b.title as title, b.author.name as authorName FROM Book b WHERE b.id = :id"
 +Optional<BookDTO> findBookDTOById(@Param("id") Long id); 
 +</sxh> 
 + 
 + 
 +===== Bonnes Pratiques ===== 
 + 
 +==== 1. Toujours initialiser les collections ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author"
 +    private List<Book> books = new ArrayList<>();  // ✅ Initialiser directement 
 +
 +</sxh> 
 + 
 +**Évite** : ''NullPointerException'' et facilite l'ajout d'éléments. 
 + 
 +==== 2. Utiliser des méthodes helper ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Author { 
 +    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true) 
 +    private List<Book> books = new ArrayList<>(); 
 +     
 +    // ✅ Encapsule la logique de synchronisation 
 +    public void addBook(Book book) { 
 +        books.add(book); 
 +        book.setAuthor(this); 
 +    } 
 +     
 +    public void removeBook(Book book) { 
 +        books.remove(book); 
 +        book.setAuthor(null); 
 +    } 
 +
 +</sxh> 
 + 
 +==== 3. Implémenter equals() et hashCode() correctement ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Book { 
 +    @Id 
 +    @GeneratedValue(strategy = GenerationType.IDENTITY) 
 +    private Long id; 
 +     
 +    @Override 
 +    public boolean equals(Object o) { 
 +        if (this == o) return true; 
 +        if (!(o instanceof Book)) return false; 
 +        Book book = (Book) o; 
 +        return id != null && id.equals(book.getId()); 
 +    } 
 +     
 +    @Override 
 +    public int hashCode() { 
 +        return getClass().hashCode();  // ✅ Stable, ne change pas 
 +    } 
 +
 +</sxh> 
 + 
 +<WRAP round important> 
 +**NE PAS utiliser tous les champs dans ''hashCode()''** : Le hash doit rester constant, même si l'entité change. 
 + 
 +**NE PAS utiliser l'ID dans ''hashCode()''** : L'ID peut être ''null'' avant persistance. 
 +</WRAP> 
 + 
 +==== 4. Utiliser Set au lieu de List pour @ManyToMany ==== 
 + 
 +<sxh java> 
 +// ✅ Recommandé 
 +@ManyToMany 
 +private Set<Course> courses = new HashSet<>(); 
 + 
 +// ❌ Éviter (problèmes avec equals/hashCode et doublons) 
 +@ManyToMany 
 +private List<Course> courses = new ArrayList<>(); 
 +</sxh> 
 + 
 +==== 5. Spécifier fetch = FetchType.LAZY partout ==== 
 + 
 +<sxh java> 
 +@ManyToOne(fetch = FetchType.LAZY)  // ✅ Toujours explicite 
 +private Author author; 
 +</sxh> 
 + 
 +==== 6. Éviter CascadeType.REMOVE sur @ManyToOne ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Book { 
 +    @ManyToOne(cascade = CascadeType.REMOVE)  // ❌ DANGEREUX ! 
 +    private Author author; 
 +
 + 
 +// Si on supprime un livre, l'auteur est aussi supprimé ! 
 +bookRepository.delete(book);  // ⚠️ Supprime aussi l'auteur 
 +</sxh> 
 + 
 +==== 7. Utiliser orphanRemoval pour les compositions ==== 
 + 
 +<sxh java> 
 +@Entity 
 +public class Order { 
 +    @OneToMany( 
 +        mappedBy = "order", 
 +        cascade = CascadeType.ALL, 
 +        orphanRemoval = true  // ✅ Supprimer les items orphelins 
 +    ) 
 +    private List<OrderItem> items = new ArrayList<>(); 
 +
 +</sxh> 
 + 
 +==== 8. Éviter les relations bidirectionnelles inutiles ==== 
 + 
 +Si vous n'avez **jamais** besoin de naviguer de ''Course'' vers ''Student'', restez unidirectionnel : 
 + 
 +<sxh java> 
 +@Entity 
 +public class Student { 
 +    @ManyToMany 
 +    private Set<Course> courses = new HashSet<>(); 
 +
 + 
 +@Entity 
 +public class Course { 
 +    // Pas de référence à Student 
 +
 +</sxh> 
 + 
 +==== 9. Utiliser @Transactional(readOnly = true) pour les lectures ==== 
 + 
 +<sxh java> 
 +@Service 
 +@Transactional(readOnly = true)  // ✅ Optimisation 
 +public class BookService { 
 +     
 +    public List<Book> getAllBooks() { 
 +        return bookRepository.findAll(); 
 +    } 
 +
 +</sxh> 
 + 
 +==== 10. Logger les requêtes SQL en développement ==== 
 + 
 +''application.properties''
 +<code> 
 +# Afficher les requêtes SQL 
 +spring.jpa.show-sql=true 
 +spring.jpa.properties.hibernate.format_sql=true 
 + 
 +# Logger les paramètres 
 +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 
 + 
 +# Détecter le problème N+1 
 +spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=10 
 +</code> 
 + 
 + 
 +===== Mémo Final ===== 
 + 
 +<WRAP round tip> 
 +**Template de relation bidirectionnelle OneToMany/ManyToOne** : 
 + 
 +<sxh java> 
 +@Entity 
 +public class Parent { 
 +    @OneToMany( 
 +        mappedBy = "parent", 
 +        cascade = CascadeType.ALL, 
 +        orphanRemoval = true, 
 +        fetch = FetchType.LAZY 
 +    ) 
 +    private List<Child> children = new ArrayList<>(); 
 +     
 +    public void addChild(Child child) { 
 +        children.add(child); 
 +        child.setParent(this); 
 +    } 
 +     
 +    public void removeChild(Child child) { 
 +        children.remove(child); 
 +        child.setParent(null); 
 +    } 
 +     
 +    @Override 
 +    public boolean equals(Object o) { 
 +        if (this == o) return true; 
 +        if (!(o instanceof Parent)) return false; 
 +        Parent parent = (Parent) o; 
 +        return id != null && id.equals(parent.getId()); 
 +    } 
 +     
 +    @Override 
 +    public int hashCode() { 
 +        return getClass().hashCode(); 
 +    } 
 +
 + 
 +@Entity 
 +public class Child { 
 +    @ManyToOne(fetch = FetchType.LAZY, optional = false) 
 +    @JoinColumn(name = "parent_id"
 +    private Parent parent; 
 +     
 +    @Override 
 +    public boolean equals(Object o) { 
 +        if (this == o) return true; 
 +        if (!(o instanceof Child)) return false; 
 +        Child child = (Child) o; 
 +        return id != null && id.equals(child.getId()); 
 +    } 
 +     
 +    @Override 
 +    public int hashCode() { 
 +        return getClass().hashCode(); 
 +    } 
 +
 +</sxh>
 </WRAP> </WRAP>
  
  • framework-web/spring/relations.1759840046.txt.gz
  • Dernière modification : il y a 10 heures
  • de jcheron