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 15:14] – [@ManyToMany] jcheron | framework-web:spring:relations [2025/10/08 01:24] (Version actuelle) – [Schéma de base de données] jcheron | ||
|---|---|---|---|
| Ligne 1: | Ligne 1: | ||
| ====== Relations JPA ====== | ====== Relations JPA ====== | ||
| - | ===== Les Types de Relations ===== | + | ===== Types de Relations ===== |
| ==== @OneToMany / @ManyToOne ==== | ==== @OneToMany / @ManyToOne ==== | ||
| 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 160: | Ligne 160: | ||
| Ce n'est pas vraiment une ManyToMany... | Ce n'est pas vraiment une ManyToMany... | ||
| - | === Avec EmbeddedId === | + | === Avec @EmbeddedId === |
| <sxh java> | <sxh java> | ||
| Ligne 201: | Ligne 201: | ||
| </ | </ | ||
| - | === Sans , mais avec Id supplémentaire === | + | === Sans @EmbeddedId, mais avec Id supplémentaire |
| <sxh java> | <sxh java> | ||
| Ligne 264: | Ligne 264: | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | <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** : | ||
| + | <sxh 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 ! | ||
| + | |||
| + | <sxh 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** : | ||
| + | |||
| + | <sxh 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 ! | ||
| + | </ | ||
| + | |||
| + | <sxh 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()); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <sxh 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< | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <sxh 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 ==== | ||
| + | |||
| + | '' | ||
| + | <sxh bash> | ||
| + | # 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(); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | </ | ||