Table des matières

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 :

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 @Transactional sur 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 :

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 parent
  • orphanRemoval : 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 : 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 !

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 :

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)