Différences
Ci-dessous, les différences entre deux révisions de la page.
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:57] – jcheron | framework-web:spring:relations [2025/10/07 15:28] (Version actuelle) – jcheron | ||
---|---|---|---|
Ligne 7: | Ligne 7: | ||
Cas d' | Cas d' | ||
- | === Configuration Unidirectionnelle (Rarement recommandée) === | + | === Configuration Unidirectionnelle (❌ Rarement recommandée) === |
<sxh java> | <sxh java> | ||
@Entity | @Entity | ||
Ligne 29: | Ligne 29: | ||
</ | </ | ||
<WRAP round important> | <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. | Lancés automatiquement par Hibernate pour mettre à jour la clé étrangère. | ||
Ligne 35: | Ligne 35: | ||
- | === Configuration Bidirectionnelle (Recommandée) === | + | === Configuration Bidirectionnelle (✅ Recommandée) === |
<sxh java> | <sxh java> | ||
@Entity | @Entity | ||
Ligne 156: | Ligne 156: | ||
} | } | ||
</ | </ | ||
+ | |||
+ | ==== ManyToMany avec attributs ==== | ||
+ | Ce n'est pas vraiment une ManyToMany... | ||
+ | |||
+ | === Avec @EmbeddedId === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Enrollment { | ||
+ | @EmbeddedId | ||
+ | private EnrollmentId id; | ||
+ | | ||
+ | @ManyToOne | ||
+ | @MapsId(" | ||
+ | private Student student; | ||
+ | | ||
+ | @ManyToOne | ||
+ | @MapsId(" | ||
+ | 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 = " | ||
+ | private Set< | ||
+ | } | ||
+ | |||
+ | @Entity | ||
+ | public class Course { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private Set< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Sans @EmbeddedId, | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | @Table( | ||
+ | name = " | ||
+ | indexes = { | ||
+ | @Index( | ||
+ | name = " | ||
+ | columnList = " | ||
+ | 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 = " | ||
+ | private Student student; | ||
+ | | ||
+ | @ManyToOne(fetch = FetchType.LAZY) | ||
+ | @JoinColumn(name = " | ||
+ | private Course course; | ||
+ | | ||
+ | private Integer grade; | ||
+ | | ||
+ | private LocalDate enrollmentDate; | ||
+ | | ||
+ | // Constructeurs | ||
+ | | ||
+ | // Getters/ | ||
+ | | ||
+ | // 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 " | ||
+ | " | ||
+ | ", student=" | ||
+ | ", course=" | ||
+ | ", grade=" | ||
+ | ", enrollmentDate=" | ||
+ | ' | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <WRAP round tip> | ||
+ | **Avantages de cette approche** : | ||
+ | * Code simple et lisible | ||
+ | * Pas de classe @Embeddable à gérer | ||
+ | * Fonctionne parfaitement avec Spring Data | ||
+ | * L' | ||
+ | * Performance identique à une PK composite | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== FetchType : LAZY vs EAGER ===== | ||
+ | |||
+ | ==== LAZY (✅ Recommandé par défaut) ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Book { | ||
+ | @ManyToOne(fetch = FetchType.LAZY) | ||
+ | private Author author; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **Comportement** : | ||
+ | * Les données ne sont chargées **que si on y accède** | ||
+ | * Évite les requêtes inutiles | ||
+ | * Peut lancer une '' | ||
+ | |||
+ | **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) | ||
+ | </ | ||
+ | |||
+ | <WRAP round important> | ||
+ | **LazyInitializationException** : Se produit quand on accède à une relation LAZY en dehors d'une transaction. | ||
+ | |||
+ | **Solutions** : | ||
+ | * Utiliser '' | ||
+ | * Utiliser un '' | ||
+ | * Utiliser une query avec '' | ||
+ | </ | ||
+ | |||
+ | ==== EAGER (⚠️ À utiliser avec précaution) ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Book { | ||
+ | @ManyToOne(fetch = FetchType.EAGER) | ||
+ | private Author author; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **Comportement** : | ||
+ | * Les données sont chargées **immédiatement** avec l' | ||
+ | * 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 ^ | ||
+ | | '' | ||
+ | | '' | ||
+ | | '' | ||
+ | | '' | ||
+ | |||
+ | <WRAP round tip> | ||
+ | **Bonne pratique** : Toujours spécifier explicitement le '' | ||
+ | </ | ||
+ | |||
+ | ==== Optimiser avec JOIN FETCH ==== | ||
+ | |||
+ | <sxh java> | ||
+ | public interface BookRepository extends JpaRepository< | ||
+ | | ||
+ | // ✅ Charge Book + Author en une seule requête | ||
+ | @Query(" | ||
+ | Optional< | ||
+ | | ||
+ | // ✅ Charge tous les Books + leurs Authors | ||
+ | @Query(" | ||
+ | List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Optimiser avec @EntityGraph ==== | ||
+ | |||
+ | <sxh java> | ||
+ | public interface BookRepository extends JpaRepository< | ||
+ | | ||
+ | @EntityGraph(attributePaths = {" | ||
+ | Optional< | ||
+ | | ||
+ | @EntityGraph(attributePaths = {" | ||
+ | List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Cascade ===== | ||
+ | |||
+ | Les '' | ||
+ | |||
+ | ==== Les différents types ==== | ||
+ | |||
+ | <sxh java> | ||
+ | public enum CascadeType { | ||
+ | PERSIST, | ||
+ | MERGE, | ||
+ | REMOVE, | ||
+ | REFRESH, | ||
+ | DETACH, | ||
+ | ALL // Tous les types ci-dessus | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Exemples ==== | ||
+ | |||
+ | === PERSIST : Propager la création === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.PERSIST | ||
+ | ) | ||
+ | private List< | ||
+ | } | ||
+ | |||
+ | // Utilisation | ||
+ | Author author = new Author(" | ||
+ | Book book1 = new Book(" | ||
+ | Book book2 = new Book(" | ||
+ | |||
+ | author.addBook(book1); | ||
+ | author.addBook(book2); | ||
+ | |||
+ | entityManager.persist(author); | ||
+ | // ✅ author, book1 et book2 sont tous persistés automatiquement | ||
+ | </ | ||
+ | |||
+ | === REMOVE : Propager la suppression (⚠️ Dangereux) === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.REMOVE | ||
+ | ) | ||
+ | private List< | ||
+ | } | ||
+ | |||
+ | // Utilisation | ||
+ | Author author = authorRepository.findById(1L).get(); | ||
+ | authorRepository.delete(author); | ||
+ | // ⚠️ Tous les livres de cet auteur sont aussi supprimés ! | ||
+ | </ | ||
+ | |||
+ | <WRAP round important> | ||
+ | **'' | ||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | **Exemple** : | ||
+ | <sxh java> | ||
+ | // Avec CascadeType.REMOVE uniquement | ||
+ | author.getBooks().remove(book); | ||
+ | |||
+ | // Avec orphanRemoval = true | ||
+ | author.getBooks().remove(book); | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | === ALL : Propager toutes les opérations === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.ALL, | ||
+ | orphanRemoval = true | ||
+ | ) | ||
+ | private List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Bonnes pratiques Cascade ==== | ||
+ | |||
+ | <WRAP round tip> | ||
+ | **Recommandations** : | ||
+ | * **'' | ||
+ | * **'' | ||
+ | * **'' | ||
+ | * **'' | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== orphanRemoval ===== | ||
+ | |||
+ | '' | ||
+ | |||
+ | ==== Exemple ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.ALL, | ||
+ | orphanRemoval = true // ← Suppression automatique des orphelins | ||
+ | ) | ||
+ | private List< | ||
+ | | ||
+ | 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 | ||
+ | </ | ||
+ | |||
+ | ==== 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' | ||
+ | |||
+ | <WRAP round tip> | ||
+ | **orphanRemoval = true** est idéal pour les relations de **composition** (parent-enfant fort) où l' | ||
+ | |||
+ | **Exemples** : Order → OrderItem, Invoice → InvoiceLine, | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== 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' | ||
+ | |||
+ | <WRAP round important> | ||
+ | **Règle absolue** : Pour qu'une modification de relation soit persistée, elle DOIT être faite côté owner. | ||
+ | </ | ||
+ | |||
+ | ==== Schéma de base de données ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private List< | ||
+ | } | ||
+ | |||
+ | @Entity | ||
+ | public class Book { | ||
+ | @ManyToOne | ||
+ | @JoinColumn(name = " | ||
+ | private Author author; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **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, | ||
+ | CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES author(id) | ||
+ | ); | ||
+ | </ | ||
+ | |||
+ | ==== Conséquence : Seul le Owner peut persister la relation ==== | ||
+ | |||
+ | === ❌ Ce qui NE fonctionne PAS === | ||
+ | |||
+ | <sxh java> | ||
+ | Author author = new Author(" | ||
+ | Book book = new 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 ! | ||
+ | </ | ||
+ | |||
+ | **Pourquoi ?** Hibernate ignore le côté '' | ||
+ | |||
+ | === ✅ La bonne méthode === | ||
+ | |||
+ | <sxh java> | ||
+ | Author author = new Author(" | ||
+ | Book book = new 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 ✅ | ||
+ | </ | ||
+ | |||
+ | === ✅ La méthode recommandée : Méthodes helper === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private List< | ||
+ | | ||
+ | // ✅ Méthode qui synchronise les deux côtés | ||
+ | public void addBook(Book book) { | ||
+ | books.add(book); | ||
+ | book.setAuthor(this); | ||
+ | } | ||
+ | | ||
+ | public void removeBook(Book book) { | ||
+ | books.remove(book); | ||
+ | book.setAuthor(null); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Utilisation | ||
+ | Author author = new Author(" | ||
+ | Book book = new Book(" | ||
+ | |||
+ | author.addBook(book); | ||
+ | |||
+ | authorRepository.save(author); | ||
+ | </ | ||
+ | |||
+ | ==== Performance : Unidirectionnel vs Bidirectionnel ==== | ||
+ | |||
+ | === Configuration Unidirectionnelle @OneToMany (❌ Mauvaise perf) === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany | ||
+ | @JoinColumn(name = " | ||
+ | private List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **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; | ||
+ | </ | ||
+ | |||
+ | === Configuration Bidirectionnelle (✅ Meilleure perf) === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private List< | ||
+ | } | ||
+ | |||
+ | @Entity | ||
+ | public class Book { | ||
+ | @ManyToOne | ||
+ | @JoinColumn(name = " | ||
+ | private Author author; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **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' | ||
+ | </ | ||
+ | |||
+ | ==== ManyToMany : Qui est le owner ? ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Student { | ||
+ | @ManyToMany | ||
+ | @JoinTable( | ||
+ | name = " | ||
+ | joinColumns = @JoinColumn(name = " | ||
+ | inverseJoinColumns = @JoinColumn(name = " | ||
+ | ) | ||
+ | private Set< | ||
+ | } | ||
+ | |||
+ | @Entity | ||
+ | public class Course { | ||
+ | @ManyToMany(mappedBy = " | ||
+ | private Set< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **Conséquence** : Pour ajouter une relation, il faut modifier '' | ||
+ | |||
+ | <sxh java> | ||
+ | // ✅ Modifier le owner | ||
+ | student.getCourses().add(course); | ||
+ | studentRepository.save(student); | ||
+ | |||
+ | // ❌ Modifier l' | ||
+ | course.getStudents().add(student); | ||
+ | courseRepository.save(course); | ||
+ | </ | ||
+ | |||
+ | <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); | ||
+ | course.getStudents().add(this); | ||
+ | } | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Queries et Problèmes Courants ===== | ||
+ | |||
+ | ==== Problème N+1 ==== | ||
+ | |||
+ | Le problème N+1 se produit quand on charge une collection d' | ||
+ | |||
+ | === Exemple du problème === | ||
+ | |||
+ | <sxh java> | ||
+ | // 1 requête pour charger tous les livres | ||
+ | List< | ||
+ | |||
+ | // N requêtes supplémentaires (une par livre) pour charger les auteurs ! | ||
+ | for (Book book : books) { | ||
+ | System.out.println(book.getAuthor().getName()); | ||
+ | } | ||
+ | |||
+ | // Total : 1 + N requêtes ! | ||
+ | </ | ||
+ | |||
+ | <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 | ||
+ | ... | ||
+ | </ | ||
+ | |||
+ | === Solution 1 : JOIN FETCH === | ||
+ | |||
+ | <sxh java> | ||
+ | public interface BookRepository extends JpaRepository< | ||
+ | | ||
+ | @Query(" | ||
+ | List< | ||
+ | } | ||
+ | |||
+ | // Utilisation | ||
+ | List< | ||
+ | // ✅ 1 seule requête SQL avec JOIN ! | ||
+ | |||
+ | for (Book book : books) { | ||
+ | System.out.println(book.getAuthor().getName()); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <code sql> | ||
+ | -- ✅ Une seule requête ! | ||
+ | SELECT b.*, a.* | ||
+ | FROM book b | ||
+ | INNER JOIN author a ON b.author_id = a.id; | ||
+ | </ | ||
+ | |||
+ | === Solution 2 : @EntityGraph === | ||
+ | |||
+ | <sxh java> | ||
+ | public interface BookRepository extends JpaRepository< | ||
+ | | ||
+ | @EntityGraph(attributePaths = {" | ||
+ | List< | ||
+ | | ||
+ | @EntityGraph(attributePaths = {" | ||
+ | Optional< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Solution 3 : @Fetch(FetchMode.SUBSELECT) === | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | @Fetch(FetchMode.SUBSELECT) | ||
+ | private List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <code sql> | ||
+ | SELECT * FROM author; | ||
+ | |||
+ | -- Requête 2 : charge tous les livres en une fois | ||
+ | SELECT * FROM book WHERE author_id IN ( | ||
+ | SELECT id FROM author | ||
+ | ); | ||
+ | </ | ||
+ | |||
+ | ==== LazyInitializationException ==== | ||
+ | |||
+ | **Erreur** : '' | ||
+ | |||
+ | **Cause** : Accès à une relation LAZY en dehors d'une transaction/ | ||
+ | |||
+ | === 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 ! | ||
+ | </ | ||
+ | |||
+ | === 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(); | ||
+ | return book; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Solution 2 : JOIN FETCH === | ||
+ | |||
+ | <sxh java> | ||
+ | @Query(" | ||
+ | Optional< | ||
+ | </ | ||
+ | |||
+ | === Solution 3 : @EntityGraph === | ||
+ | |||
+ | <sxh java> | ||
+ | @EntityGraph(attributePaths = {" | ||
+ | Optional< | ||
+ | </ | ||
+ | |||
+ | === Solution 4 : DTO Projection === | ||
+ | |||
+ | <sxh java> | ||
+ | public interface BookDTO { | ||
+ | Long getId(); | ||
+ | String getTitle(); | ||
+ | String getAuthorName(); | ||
+ | } | ||
+ | |||
+ | @Query(" | ||
+ | Optional< | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Bonnes Pratiques ===== | ||
+ | |||
+ | ==== 1. Toujours initialiser les collections ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | **Évite** : '' | ||
+ | |||
+ | ==== 2. Utiliser des méthodes helper ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Author { | ||
+ | @OneToMany(mappedBy = " | ||
+ | private List< | ||
+ | | ||
+ | // ✅ 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); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 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(); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | <WRAP round important> | ||
+ | **NE PAS utiliser tous les champs dans '' | ||
+ | |||
+ | **NE PAS utiliser l'ID dans '' | ||
+ | </ | ||
+ | |||
+ | ==== 4. Utiliser Set au lieu de List pour @ManyToMany ==== | ||
+ | |||
+ | <sxh java> | ||
+ | // ✅ Recommandé | ||
+ | @ManyToMany | ||
+ | private Set< | ||
+ | |||
+ | // ❌ Éviter (problèmes avec equals/ | ||
+ | @ManyToMany | ||
+ | private List< | ||
+ | </ | ||
+ | |||
+ | ==== 5. Spécifier fetch = FetchType.LAZY partout ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @ManyToOne(fetch = FetchType.LAZY) | ||
+ | private Author author; | ||
+ | </ | ||
+ | |||
+ | ==== 6. Éviter CascadeType.REMOVE sur @ManyToOne ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Book { | ||
+ | @ManyToOne(cascade = CascadeType.REMOVE) | ||
+ | private Author author; | ||
+ | } | ||
+ | |||
+ | // Si on supprime un livre, l' | ||
+ | bookRepository.delete(book); | ||
+ | </ | ||
+ | |||
+ | ==== 7. Utiliser orphanRemoval pour les compositions ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Order { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.ALL, | ||
+ | orphanRemoval = true // ✅ Supprimer les items orphelins | ||
+ | ) | ||
+ | private List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 8. Éviter les relations bidirectionnelles inutiles ==== | ||
+ | |||
+ | Si vous n'avez **jamais** besoin de naviguer de '' | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Student { | ||
+ | @ManyToMany | ||
+ | private Set< | ||
+ | } | ||
+ | |||
+ | @Entity | ||
+ | public class Course { | ||
+ | // Pas de référence à Student | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 9. Utiliser @Transactional(readOnly = true) pour les lectures ==== | ||
+ | |||
+ | <sxh java> | ||
+ | @Service | ||
+ | @Transactional(readOnly = true) // ✅ Optimisation | ||
+ | public class BookService { | ||
+ | | ||
+ | public List< | ||
+ | return bookRepository.findAll(); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 10. Logger les requêtes SQL en développement ==== | ||
+ | |||
+ | '' | ||
+ | < | ||
+ | # 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 | ||
+ | </ | ||
+ | |||
+ | |||
+ | ===== Mémo Final ===== | ||
+ | |||
+ | <WRAP round tip> | ||
+ | **Template de relation bidirectionnelle OneToMany/ | ||
+ | |||
+ | <sxh java> | ||
+ | @Entity | ||
+ | public class Parent { | ||
+ | @OneToMany( | ||
+ | mappedBy = " | ||
+ | cascade = CascadeType.ALL, | ||
+ | orphanRemoval = true, | ||
+ | fetch = FetchType.LAZY | ||
+ | ) | ||
+ | private List< | ||
+ | | ||
+ | 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, | ||
+ | @JoinColumn(name = " | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | </ | ||
+ |