Relations JPA
Types de Relations
@OneToMany / @ManyToOne
Cas d'usage : Un auteur a plusieurs livres, un livre a un seul auteur.
Configuration Unidirectionnelle (🟰 Rarement recommandée)
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany
@JoinColumn(name = "author_id") // Crée une FK dans Book
private List<Book> books = new ArrayList<>();
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Pas de référence à Author
}
Problème : Génère des UPDATE supplémentaires !
Lancés automatiquement par Hibernate pour mettre à jour la clé étrangère.
Configuration Bidirectionnelle (✅ Recommandée)
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(
mappedBy = "author", // Référence au champ dans Book
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY // Par défaut
)
private List<Book> books = new ArrayList<>();
// Méthodes helper pour synchroniser 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);
}
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(
fetch = FetchType.LAZY, // Recommandé
optional = false // NOT NULL en base
)
@JoinColumn(name = "author_id") // Nom de la FK
private Author author;
}
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.
@ManyToMany
Cas d'usage : Un étudiant suit plusieurs cours, un cours a plusieurs étudiants.
Configuration Unidirectionnelle
@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
}
Configuration Bidirectionnelle (✅ Recommandée)
@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<>();
}
ManyToMany avec attributs
Ce n'est pas vraiment une ManyToMany…
Avec @EmbeddedId
@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<>();
}
Sans @EmbeddedId, mais avec Id supplémentaire (✅ Recommandée)
@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 +
'}';
}
}
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
FetchType : LAZY vs EAGER
LAZY (✅ Recommandé par défaut)
@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
LazyInitializationExceptionsi la session Hibernate est fermée
Exemple :
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)
LazyInitializationException : Se produit quand on accède à une relation LAZY en dehors d'une transaction.
Solutions :
- Utiliser
@Transactionalsur la méthode - Utiliser un
@EntityGraph - Utiliser une query avec
JOIN FETCH
EAGER (⚠️ À utiliser avec précaution)
@Entity
public class Book {
@ManyToOne(fetch = FetchType.EAGER)
private Author author;
}
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 |
Bonne pratique : Toujours spécifier explicitement le FetchType pour éviter les surprises.
Optimiser avec JOIN FETCH
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();
}
Optimiser avec @EntityGraph
public interface BookRepository extends JpaRepository<Book, Long> {
@EntityGraph(attributePaths = {"author"})
Optional<Book> findById(Long id);
@EntityGraph(attributePaths = {"author", "publisher"})
List<Book> findAll();
}
Cascade
Les CascadeType définissent quelles opérations sont propagées aux entités liées.
Les différents types
public enum CascadeType {
PERSIST, // entityManager.persist()
MERGE, // entityManager.merge()
REMOVE, // entityManager.remove()
REFRESH, // entityManager.refresh()
DETACH, // entityManager.detach()
ALL // Tous les types ci-dessus
}
Exemples
PERSIST : Propager la création
@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
REMOVE : Propager la suppression (⚠️ Dangereux)
@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 !
CascadeType.REMOVE vs orphanRemoval :
REMOVE: Supprime les entités liées quand on supprime le parentorphanRemoval: Supprime les entités liées quand on les retire de la collection
Exemple :
// 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
ALL : Propager toutes les opérations
@Entity
public class Author {
@OneToMany(
mappedBy = "author",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Book> books = new ArrayList<>();
}
Bonnes pratiques Cascade
Recommandations :
@OneToMany: UtiliserCascadeType.ALL+orphanRemoval = truepour les relations de composition (ex: Order → OrderItems)@ManyToOne: NE PAS utiliser de cascade (sauf cas très particuliers)@ManyToMany: UtiliserCascadeType.PERSISTetCascadeType.MERGEuniquementCascadeType.REMOVE: Attention aux suppressions en cascade non voulues !
orphanRemoval
orphanRemoval = true supprime automatiquement les entités “orphelines” (qui ne sont plus dans la collection parente).
Exemple
@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
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) |
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
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
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
@Entity
public class Author {
@OneToMany(mappedBy = "author") // ❌ Non owner
private List<Book> books;
}
@Entity
public class Book {
@ManyToOne
@JoinColumn(name = "author_id") // ✅ Owner
private Author author;
}
En base de données :
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
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 !
Pourquoi ? Hibernate ignore le côté @OneToMany sans mappedBy pour la persistance.
✅ La bonne méthode
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 ✅
✅ La méthode recommandée : Méthodes helper
@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
Performance : Unidirectionnel vs Bidirectionnel
Configuration Unidirectionnelle @OneToMany (❌ Mauvaise perf)
@Entity
public class Author {
@OneToMany
@JoinColumn(name = "author_id")
private List<Book> books = new ArrayList<>();
}
Problème : Génère des requêtes supplémentaires !
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)
@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;
}
Requêtes optimisées :
INSERT INTO author (name) VALUES ('John Doe');
INSERT INTO book (title, author_id) VALUES ('Book 1', 1)
- framework-web/spring/relations.txt
- Dernière modification : il y a 3 mois
- de
jcheron