Public cible : Développeur.euse.s Java ayant des bases en JPA/Hibernate Prérequis : Java 17+, Spring Boot, notions SQL, mapping JPA/Hibernate
JPA (Java Persistence API) est une spécification Java (aujourd’hui Jakarta EE) qui définit comment mapper des objets Java vers des tables de base de données relationnelle. C’est un standard : il ne s’exécute pas tout seul, il a besoin d’une implémentation.
Hibernate est l’implémentation la plus populaire de JPA. Quand vous utilisez Spring Boot avec spring-boot-starter-data-jpa, Hibernate est inclus automatiquement.
spring-boot-starter-data-jpa
Analogie : JPA est comme une interface Java : elle définit le contrat. Hibernate est l’implémentation concrète de ce contrat, comme une classe qui implements cette interface.
implements
[ Application Java ] ↕ [ JPA (API standard) ] ← vous écrivez votre code contre JPA ↕ [ Hibernate (implémentation) ] ← traduit vos appels JPA en SQL ↕ [ JDBC Driver PostgreSQL ] ↕ [ Base de données PostgreSQL ]
Dans pom.xml :
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> </dependencies>
Dans application.properties :
application.properties
# Connexion PostgreSQL spring.datasource.url=jdbc:postgresql://localhost:5432/ma_base spring.datasource.username=postgres spring.datasource.password=secret spring.datasource.driver-class-name=org.postgresql.Driver # Hibernate spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
ddl-auto=update est pratique en développement mais dangereux en production. En production, utilisez validate ou none et gérez les migrations avec Flyway/Liquibase.
ddl-auto=update
validate
none
@Entity
Transforme une classe Java en entité JPA, c’est-à-dire en objet qui sera persisté en base.
@Entity @Table(name = "produits") // optionnel, si le nom de table diffère du nom de classe public class Produit { // ... }
@Entity public class Produit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // IDENTITY : délègue la génération à PostgreSQL (SERIAL / BIGSERIAL) // SEQUENCE : utilise une séquence SQL (recommandé avec PostgreSQL) // AUTO : Hibernate choisit automatiquement private Long id; @Column(name = "nom_produit", nullable = false, length = 100) private String nom; @Column(precision = 10, scale = 2) private BigDecimal prix; @Column(name = "date_creation") private LocalDate dateCreation; // getters et setters... }
Voici le modèle de données que nous allons utiliser pour tous nos exemples. Il représente une librairie en ligne :
// --- Entité Auteur --- @Entity @Table(name = "auteurs") public class Auteur { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String nom; @Column(nullable = false) private String prenom; private String nationalite; @OneToMany(mappedBy = "auteur", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Livre> livres = new ArrayList<>(); // constructeurs, getters, setters... } // --- Entité Categorie --- @Entity @Table(name = "categories") public class Categorie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String libelle; @OneToMany(mappedBy = "categorie") private List<Livre> livres = new ArrayList<>(); } // --- Entité Livre --- @Entity @Table(name = "livres") public class Livre { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String titre; @Column(precision = 8, scale = 2) private BigDecimal prix; private Integer anneePublication; private Integer stock; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "auteur_id") private Auteur auteur; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "categorie_id") private Categorie categorie; @ManyToMany @JoinTable( name = "livre_commande", joinColumns = @JoinColumn(name = "livre_id"), inverseJoinColumns = @JoinColumn(name = "commande_id") ) private List<Commande> commandes = new ArrayList<>(); } // --- Entité Client --- @Entity @Table(name = "clients") public class Client { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nom; private String email; private String ville; @OneToMany(mappedBy = "client", cascade = CascadeType.ALL) private List<Commande> commandes = new ArrayList<>(); } // --- Entité Commande --- @Entity @Table(name = "commandes") public class Commande { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private LocalDate dateCommande; @Enumerated(EnumType.STRING) private StatutCommande statut; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "client_id") private Client client; @ManyToMany(mappedBy = "commandes") private List<Livre> livres = new ArrayList<>(); } // --- Enum --- public enum StatutCommande { EN_ATTENTE, VALIDEE, EXPEDIEE, LIVREE, ANNULEE }
C’est un concept crucial pour comprendre les performances JPA.
FetchType.LAZY (paresseux) : Les données associées ne sont pas chargées immédiatement. Elles ne sont récupérées que lorsqu’on y accède dans le code. C’est le comportement par défaut pour @OneToMany et @ManyToMany.
FetchType.LAZY
@OneToMany
@ManyToMany
FetchType.EAGER (on peut traduire par avide) : Les données associées sont immédiatement chargées avec l’entité parente. C’est le comportement par défaut pour @ManyToOne et @OneToOne.
FetchType.EAGER
@ManyToOne
@OneToOne
// Avec LAZY : quand on charge un Livre, l'Auteur n'est PAS chargé // Il ne sera chargé que quand on appellera livre.getAuteur() @ManyToOne(fetch = FetchType.LAZY) private Auteur auteur; // Avec EAGER : quand on charge un Livre, l'Auteur est chargé EN MÊME TEMPS @ManyToOne(fetch = FetchType.EAGER) private Auteur auteur;
Bonne pratique : Préférez toujours LAZY par défaut. EAGER peut provoquer des cascades de requêtes non désirées (le fameux problème N+1 que nous verrons plus tard).
LAZY
EAGER
JPQL (Java Persistence Query Language) est un langage de requêtes orienté objets défini par la spécification JPA. Il ressemble à SQL, mais avec une différence fondamentale : vous travaillez avec des entités et leurs champs Java, pas avec des tables et colonnes SQL.
SELECT * FROM livres
SELECT l FROM Livre l
SELECT nom FROM auteurs
SELECT a.nom FROM Auteur a
WHERE auteur_id = 5
WHERE l.auteur.id = 5
JOIN auteurs ON livres.auteur_id = auteurs.id
JOIN l.auteur a
Point clé : En JPQL, vous écrivez Livre (le nom de la classe Java) et non livres (le nom de la table SQL). Les noms sont sensibles à la casse pour les entités et leurs champs.
Livre
livres
l.auteur.nom
On utilise l’EntityManager, qui est le cœur de JPA : c’est lui qui gère le cycle de vie des entités et exécute les requêtes.
EntityManager
@Repository public class LivreRepository { @PersistenceContext private EntityManager em; // Injecté par Spring/JPA public List<Livre> findAll() { // createQuery() prend la requête JPQL et le type de retour TypedQuery<Livre> query = em.createQuery( "SELECT l FROM Livre l", Livre.class ); return query.getResultList(); } }
Note : Avec Spring Data JPA (que nous verrons au chapitre 8), on n’a généralement pas besoin d’utiliser directement l’EntityManager. Mais le comprendre est essentiel pour maîtriser ce qui se passe en coulisses.
// Récupérer tous les livres TypedQuery<Livre> query = em.createQuery( "SELECT l FROM Livre l", Livre.class ); List<Livre> livres = query.getResultList();
Le mot l après Livre est un alias (comme en SQL). C’est une variable qui représente chaque instance de Livre. On peut choisir n’importe quel nom, mais la convention est d’utiliser l’initiale de l’entité.
l
// Récupérer uniquement les titres (retourne une List<String>) TypedQuery<String> query = em.createQuery( "SELECT l.titre FROM Livre l", String.class ); List<String> titres = query.getResultList();
// Récupérer titre et prix (retourne une List<Object[]>) TypedQuery<Object[]> query = em.createQuery( "SELECT l.titre, l.prix FROM Livre l", Object[].class ); List<Object[]> resultats = query.getResultList(); for (Object[] row : resultats) { String titre = (String) row[0]; BigDecimal prix = (BigDecimal) row[1]; System.out.println(titre + " - " + prix + " €"); }
La manipulation de Object[] est peu pratique et source d’erreurs. Nous verrons comment utiliser des DTOs au chapitre 11 pour une approche plus propre.
Object[]
// Livres coûtant moins de 20 € em.createQuery("SELECT l FROM Livre l WHERE l.prix < 20", Livre.class) .getResultList(); // Livres d'une année précise em.createQuery("SELECT l FROM Livre l WHERE l.anneePublication = 2023", Livre.class) .getResultList(); // Livres dont le titre est exactement "Dune" em.createQuery("SELECT l FROM Livre l WHERE l.titre = 'Dune'", Livre.class) .getResultList();
Attention : Dans ces exemples, les valeurs sont en dur dans la requête. C’est une mauvaise pratique (risque d’injection, pas réutilisable). Nous allons voir les paramètres juste après.
LIKE fonctionne comme en SQL :
LIKE
%
_
// Livres dont le titre commence par "Le" em.createQuery("SELECT l FROM Livre l WHERE l.titre LIKE 'Le%'", Livre.class) .getResultList(); // Livres dont le titre contient "Java" em.createQuery("SELECT l FROM Livre l WHERE l.titre LIKE '%Java%'", Livre.class) .getResultList();
// Livres publiés entre 2000 et 2010 em.createQuery( "SELECT l FROM Livre l WHERE l.anneePublication BETWEEN 2000 AND 2010", Livre.class ).getResultList();
// Livres publiés en 2020, 2021 ou 2022 em.createQuery( "SELECT l FROM Livre l WHERE l.anneePublication IN (2020, 2021, 2022)", Livre.class ).getResultList();
// Livres sans catégorie assignée em.createQuery( "SELECT l FROM Livre l WHERE l.categorie IS NULL", Livre.class ).getResultList(); // Livres avec catégorie assignée em.createQuery( "SELECT l FROM Livre l WHERE l.categorie IS NOT NULL", Livre.class ).getResultList();
// Livres de science-fiction coûtant moins de 15 € em.createQuery( "SELECT l FROM Livre l " + "WHERE l.categorie.libelle = 'Science-Fiction' " + "AND l.prix < 15", Livre.class ).getResultList(); // Livres très chers OU épuisés em.createQuery( "SELECT l FROM Livre l " + "WHERE l.prix > 50 OR l.stock = 0", Livre.class ).getResultList();
// Livres triés par prix croissant em.createQuery( "SELECT l FROM Livre l ORDER BY l.prix ASC", Livre.class ).getResultList(); // Livres triés par année décroissante, puis par titre croissant em.createQuery( "SELECT l FROM Livre l ORDER BY l.anneePublication DESC, l.titre ASC", Livre.class ).getResultList();
DISTINCT élimine les doublons dans les résultats.
DISTINCT
// Toutes les années de publication distinctes em.createQuery( "SELECT DISTINCT l.anneePublication FROM Livre l ORDER BY l.anneePublication", Integer.class ).getResultList();
Écrire des valeurs directement dans les requêtes est une très mauvaise pratique :
Toujours utiliser des paramètres pour injecter les valeurs dynamiques.
Les paramètres nommés utilisent la syntaxe :nomDuParametre.
:nomDuParametre
// Recherche par titre exact public List<Livre> findByTitre(String titre) { return em.createQuery( "SELECT l FROM Livre l WHERE l.titre = :titre", Livre.class ) .setParameter("titre", titre) // on lie le paramètre ici .getResultList(); } // Recherche dans une plage de prix public List<Livre> findByPrixEntre(BigDecimal min, BigDecimal max) { return em.createQuery( "SELECT l FROM Livre l WHERE l.prix BETWEEN :prixMin AND :prixMax", Livre.class ) .setParameter("prixMin", min) .setParameter("prixMax", max) .getResultList(); } // Recherche partielle (LIKE avec paramètre) public List<Livre> findByTitreContenant(String motCle) { return em.createQuery( "SELECT l FROM Livre l WHERE l.titre LIKE :motCle", Livre.class ) // On concatène le % en Java, pas dans la requête JPQL .setParameter("motCle", "%" + motCle + "%") .getResultList(); }
Ils utilisent ?1, ?2, etc. Moins lisibles, mais vous pouvez les rencontrer dans du code existant.
?1
?2
// Déconseillé : ordre des paramètres fragile em.createQuery( "SELECT l FROM Livre l WHERE l.titre = ?1 AND l.prix < ?2", Livre.class ) .setParameter(1, "Dune") .setParameter(2, new BigDecimal("20.00")) .getResultList();
Quand on attend exactement un seul résultat :
public Livre findById(Long id) { try { return em.createQuery( "SELECT l FROM Livre l WHERE l.id = :id", Livre.class ) .setParameter("id", id) .getSingleResult(); // lève une exception si 0 ou 2+ résultats ! } catch (NoResultException e) { return null; // ou lancer votre propre exception métier } }
getSingleResult() lève NoResultException si aucun résultat, et NonUniqueResultException si plusieurs. Gérez toujours ces cas.
getSingleResult()
NoResultException
NonUniqueResultException
On peut pré-définir des requêtes JPQL directement sur l’entité avec @NamedQuery. Elles sont compilées au démarrage de l’application (détection d’erreurs plus tôt) et mises en cache.
@NamedQuery
@Entity @Table(name = "livres") @NamedQuery( name = "Livre.findByCategorie", query = "SELECT l FROM Livre l WHERE l.categorie.libelle = :libelle" ) @NamedQuery( name = "Livre.findCheap", query = "SELECT l FROM Livre l WHERE l.prix < 15 ORDER BY l.prix ASC" ) public class Livre { // ... }
Utilisation :
public List<Livre> findByCategorie(String libelle) { return em.createNamedQuery("Livre.findByCategorie", Livre.class) .setParameter("libelle", libelle) .getResultList(); }
Avec Spring Data JPA, on utilise plutôt @Query directement sur les méthodes du Repository. Mais @NamedQuery reste utile pour centraliser les requêtes avec l’entité.
@Query
En SQL, on écrit des jointures entre tables en précisant les colonnes de liaison :
SELECT l.titre, a.nom FROM livres l JOIN auteurs a ON l.auteur_id = a.id
En JPQL, on navigue dans le graphe d’objets via les relations déjà mappées. Pas besoin de préciser la condition de jointure : JPA la connaît déjà grâce aux annotations (@ManyToOne, @OneToMany, etc.).
SELECT l.titre, a.nom FROM Livre l JOIN l.auteur a
C’est beaucoup plus simple et naturel !
Un JOIN (ou INNER JOIN) ne retourne que les entités pour lesquelles la relation existe. Si un livre n’a pas d’auteur, il n’apparaîtra pas dans les résultats.
JOIN
INNER JOIN
// Récupérer les livres avec leurs auteurs (seulement les livres qui ont un auteur) List<Object[]> resultats = em.createQuery( "SELECT l.titre, a.nom, a.prenom " + "FROM Livre l " + "JOIN l.auteur a", Object[].class ).getResultList(); for (Object[] row : resultats) { System.out.println(row[0] + " par " + row[2] + " " + row[1]); }
// Récupérer les auteurs ayant au moins un livre List<Auteur> auteursAvecLivres = em.createQuery( "SELECT DISTINCT a FROM Auteur a JOIN a.livres l", Auteur.class ).getResultList();
Un LEFT JOIN retourne toutes les entités du côté gauche, même si la relation n’existe pas (l’entité liée sera null).
LEFT JOIN
null
// Récupérer TOUS les auteurs, avec ou sans livres List<Object[]> resultats = em.createQuery( "SELECT a.nom, a.prenom, l.titre " + "FROM Auteur a " + "LEFT JOIN a.livres l", Object[].class ).getResultList(); // Si un auteur n'a pas de livres, l.titre sera null dans les résultats
C’est le problème de performance le plus courant en JPA. Il survient quand on charge une liste d’entités avec des relations LAZY, puis qu’on accède à ces relations dans une boucle.
// Le dit PROBLÈME N+1 : 1 requête pour charger les auteurs // + N requêtes (une par auteur) pour charger leurs livres List<Auteur> auteurs = em.createQuery( "SELECT a FROM Auteur a", Auteur.class ).getResultList(); for (Auteur a : auteurs) { // Cette ligne déclenche une requête SQL à chaque itération ! System.out.println(a.getNom() + " a " + a.getLivres().size() + " livres"); }
Si on a 100 auteurs, on exécute 101 requêtes SQL au lieu d’1 !
JOIN FETCH dit à JPA de charger l’entité ET sa relation en une seule requête SQL (avec un JOIN).
JOIN FETCH
// SOLUTION : 1 seule requête SQL qui charge auteurs ET livres List<Auteur> auteurs = em.createQuery( "SELECT DISTINCT a FROM Auteur a JOIN FETCH a.livres", Auteur.class ).getResultList(); // Maintenant cette boucle n'exécute aucune requête supplémentaire for (Auteur a : auteurs) { System.out.println(a.getNom() + " a " + a.getLivres().size() + " livres"); }
Le DISTINCT est important avec JOIN FETCH sur une collection @OneToMany : sans lui, Hibernate duplique les entités parentes dans les résultats (une ligne par livre pour chaque auteur).
// JOIN FETCH avec condition WHERE List<Auteur> auteursFrancais = em.createQuery( "SELECT DISTINCT a FROM Auteur a " + "JOIN FETCH a.livres l " + "WHERE a.nationalite = :nationalite " + "AND l.anneePublication > :annee", Auteur.class ) .setParameter("nationalite", "Française") .setParameter("annee", 2000) .getResultList();
On peut enchaîner plusieurs jointures :
// Récupérer les commandes avec les clients ET les livres associés List<Commande> commandes = em.createQuery( "SELECT DISTINCT c FROM Commande c " + "JOIN FETCH c.client cl " + "JOIN FETCH c.livres l " + "WHERE cl.ville = :ville", Commande.class ) .setParameter("ville", "Paris") .getResultList();
En JPQL, on peut naviguer dans les relations directement dans les conditions WHERE sans écrire de JOIN explicite. JPA génère automatiquement le JOIN SQL nécessaire.
WHERE
// Pas besoin de JOIN explicite pour naviguer vers une relation ManyToOne List<Livre> livres = em.createQuery( "SELECT l FROM Livre l WHERE l.auteur.nationalite = 'Française'", Livre.class ).getResultList(); // Ici, Hibernate génère automatiquement un JOIN avec la table auteurs
C’est pratique pour les conditions simples, mais pour les performances (chargement de l’entité liée), préférez JOIN FETCH explicite.
Les fonctions d’agrégation calculent une valeur sur un ensemble de lignes :
COUNT(x)
SUM(x)
AVG(x)
MIN(x)
MAX(x)
// Nombre total de livres Long nbLivres = em.createQuery( "SELECT COUNT(l) FROM Livre l", Long.class ).getSingleResult(); // Prix moyen des livres Double prixMoyen = em.createQuery( "SELECT AVG(l.prix) FROM Livre l", Double.class ).getSingleResult(); // Livre le plus cher et le moins cher Object[] minMax = em.createQuery( "SELECT MIN(l.prix), MAX(l.prix) FROM Livre l", Object[].class ).getSingleResult(); System.out.println("Min: " + minMax[0] + " | Max: " + minMax[1]); // Nombre de livres d'un auteur spécifique Long nbLivresAuteur = em.createQuery( "SELECT COUNT(l) FROM Livre l WHERE l.auteur.id = :auteurId", Long.class ) .setParameter("auteurId", 1L) .getSingleResult();
GROUP BY regroupe les résultats par une ou plusieurs colonnes, souvent utilisé avec les fonctions d’agrégation.
GROUP BY
// Nombre de livres par catégorie List<Object[]> parCategorie = em.createQuery( "SELECT l.categorie.libelle, COUNT(l) " + "FROM Livre l " + "GROUP BY l.categorie.libelle " + "ORDER BY COUNT(l) DESC", Object[].class ).getResultList(); for (Object[] row : parCategorie) { System.out.println("Catégorie : " + row[0] + " | Nb livres : " + row[1]); } // Prix moyen par auteur List<Object[]> prixParAuteur = em.createQuery( "SELECT a.nom, a.prenom, AVG(l.prix), COUNT(l) " + "FROM Livre l JOIN l.auteur a " + "GROUP BY a.id, a.nom, a.prenom " + "ORDER BY a.nom", Object[].class ).getResultList();
HAVING filtre les groupes formés par GROUP BY (comme un WHERE mais appliqué après l’agrégation).
HAVING
// Catégories avec plus de 5 livres List<Object[]> grandesCategories = em.createQuery( "SELECT l.categorie.libelle, COUNT(l) " + "FROM Livre l " + "GROUP BY l.categorie.libelle " + "HAVING COUNT(l) > 5 " + "ORDER BY COUNT(l) DESC", Object[].class ).getResultList(); // Auteurs dont la moyenne de prix de leurs livres dépasse 25 € List<Object[]> auteursCher = em.createQuery( "SELECT a.nom, AVG(l.prix) " + "FROM Livre l JOIN l.auteur a " + "GROUP BY a.id, a.nom " + "HAVING AVG(l.prix) > 25", Object[].class ).getResultList();
// Livres dont le titre en majuscules contient "JAVA" em.createQuery( "SELECT l FROM Livre l WHERE UPPER(l.titre) LIKE '%JAVA%'", Livre.class ).getResultList(); // Auteurs avec leur nom complet en majuscules em.createQuery( "SELECT CONCAT(UPPER(a.prenom), ' ', UPPER(a.nom)) FROM Auteur a", String.class ).getResultList(); // Longueur des titres em.createQuery( "SELECT l.titre, LENGTH(l.titre) FROM Livre l ORDER BY LENGTH(l.titre) DESC", Object[].class ).getResultList(); // Trim : supprime les espaces en début/fin em.createQuery( "SELECT l FROM Livre l WHERE TRIM(l.titre) != ''", Livre.class ).getResultList();
// Commandes passées aujourd'hui em.createQuery( "SELECT c FROM Commande c WHERE c.dateCommande = CURRENT_DATE", Commande.class ).getResultList(); // Commandes des 30 derniers jours // Note: avec PostgreSQL, on peut aussi passer par SQL natif pour des fonctions avancées em.createQuery( "SELECT c FROM Commande c WHERE c.dateCommande >= :dateDebut", Commande.class ) .setParameter("dateDebut", LocalDate.now().minusDays(30)) .getResultList();
Une sous-requête est une requête JPQL imbriquée dans une autre. Elle s’écrit entre parenthèses dans la clause WHERE ou HAVING.
// Livres dont le prix est supérieur à la moyenne de tous les livres List<Livre> livresChers = em.createQuery( "SELECT l FROM Livre l " + "WHERE l.prix > (SELECT AVG(l2.prix) FROM Livre l2)", Livre.class ).getResultList(); // Auteurs qui ont au moins un livre publié après 2020 List<Auteur> auteursRecents = em.createQuery( "SELECT a FROM Auteur a " + "WHERE EXISTS (" + " SELECT l FROM Livre l " + " WHERE l.auteur = a AND l.anneePublication > 2020" + ")", Auteur.class ).getResultList(); // Auteurs qui n'ont AUCUN livre List<Auteur> auteursSansLivre = em.createQuery( "SELECT a FROM Auteur a " + "WHERE NOT EXISTS (" + " SELECT l FROM Livre l WHERE l.auteur = a" + ")", Auteur.class ).getResultList();
Ces opérateurs travaillent avec des sous-requêtes qui retournent plusieurs valeurs.
// Livres plus chers que TOUS les livres de la catégorie "Poche" List<Livre> livres = em.createQuery( "SELECT l FROM Livre l " + "WHERE l.prix > ALL (" + " SELECT l2.prix FROM Livre l2 " + " WHERE l2.categorie.libelle = 'Poche'" + ")", Livre.class ).getResultList(); // Livres plus chers qu'au moins UN livre de la catégorie "Poche" List<Livre> livres2 = em.createQuery( "SELECT l FROM Livre l " + "WHERE l.prix > ANY (" + " SELECT l2.prix FROM Livre l2 " + " WHERE l2.categorie.libelle = 'Poche'" + ")", Livre.class ).getResultList();
CASE permet d’ajouter de la logique conditionnelle dans les requêtes.
CASE
// Classifier les livres par gamme de prix List<Object[]> classification = em.createQuery( "SELECT l.titre, l.prix, " + "CASE " + " WHEN l.prix < 10 THEN 'Économique' " + " WHEN l.prix < 25 THEN 'Standard' " + " WHEN l.prix < 50 THEN 'Premium' " + " ELSE 'Luxe' " + "END " + "FROM Livre l", Object[].class ).getResultList(); for (Object[] row : classification) { System.out.println(row[0] + " (" + row[1] + " €) → " + row[2]); }
JPQL supporte aussi les mises à jour et suppressions en masse (bulk operations). Ces opérations agissent directement en base sans charger les entités en mémoire.
// Augmenter tous les prix de 5% int nbModifies = em.createQuery( "UPDATE Livre l SET l.prix = l.prix * 1.05" ).executeUpdate(); // Remettre à zéro le stock des livres épuisés d'avant 2010 int nbModifies2 = em.createQuery( "UPDATE Livre l SET l.stock = 100 " + "WHERE l.stock = 0 AND l.anneePublication < 2010" ).executeUpdate(); // Supprimer les commandes annulées de plus de 2 ans int nbSupprimes = em.createQuery( "DELETE FROM Commande c " + "WHERE c.statut = :statut AND c.dateCommande < :dateLimit" ) .setParameter("statut", StatutCommande.ANNULEE) .setParameter("dateLimit", LocalDate.now().minusYears(2)) .executeUpdate();
Important : Les bulk operations UPDATE et DELETE contournent le cache de premier niveau d’Hibernate. Après les avoir exécutées, rechargez les entités concernées ou appelez em.clear() pour éviter les incohérences.
UPDATE
DELETE
em.clear()
Documentation sur Spring Data JPA
Nous l’avons déjà vu, Spring Data JPA est un projet du framework Spring qui simplifie considérablement l’utilisation de JPA. Il prend en charge la couche de persistance de manière déclarative, en vous libérant de l’écriture de code répétitif (le fameux CRUD).
Avant Spring Data JPA, une couche DAO complète ressemblait à ceci (comme ce que nous avons fait au début sans la gestion de l’entityManager) :
// AVANT : code verbeux et répétitif @Repository public class LivreDao { @PersistenceContext private EntityManager em; public Livre save(Livre livre) { em.persist(livre); return livre; } public Optional<Livre> findById(Long id) { return Optional.ofNullable(em.find(Livre.class, id)); } public List<Livre> findAll() { return em.createQuery("SELECT l FROM Livre l", Livre.class).getResultList(); } public void delete(Livre livre) { em.remove(em.merge(livre)); } // ... etc. }
Avec Spring Data JPA, forcément, c’est beaucoup plus simple :
// APRÈS : une interface suffit ! public interface LivreRepository extends JpaRepository<Livre, Long> { // Spring Data JPA génère automatiquement toutes les méthodes CRUD ! }
Et Spring Boot génère automatiquement l’implémentation de cette interface au démarrage.
Spring Data JPA définit une hiérarchie d’interfaces.
Repository<T, ID> interface marqueur de base avec le Type de la classe et le type de l'ID └── CrudRepository<T, ID> opérations CRUD de base (allez voir l'interface) └── PagingAndSortingRepository<T, ID> pagination et tri en précisant la classe et le type de l'ID └── JpaRepository<T, ID> fonctionnalités JPA avancées avec les combinaisons d'attributs,...
JpaRepository<T, ID> est celle que nous avons utilisée. Juste un rappel des méthodes fournies :
JpaRepository<T, ID>
save(entity)
findById(id)
findAll()
findAll(Pageable)
count()
delete(entity)
deleteById(id)
existsById(id)
flush()
saveAndFlush()
deleteAllInBatch()
// injection dans un service @Service public class LivreService { private final LivreRepository livreRepository; // Injection par constructeur (recommandé) public LivreService(LivreRepository livreRepository) { this.livreRepository = livreRepository; } public Livre creerLivre(Livre livre) { return livreRepository.save(livre); // INSERT en base } public Optional<Livre> trouverParId(Long id) { return livreRepository.findById(id); // SELECT WHERE id = ? } public List<Livre> tousLesLivres() { return livreRepository.findAll(); // SELECT * FROM livres } public void supprimer(Long id) { livreRepository.deleteById(id); // DELETE WHERE id = ? } }
C’est la fonctionnalité la plus spectaculaire de Spring Data JPA. Il suffit d’écrire la signature d’une méthode en respectant une convention de nommage, et Spring Data JPA génère automatiquement la requête JPQL correspondante au démarrage. Elle n’est toujours optimisée mais elle fonctionne !
Documentation officielle de JPA
La méthode findByTitre(String titre) sera traduite en :
findByTitre(String titre)
SELECT l FROM Livre l WHERE l.titre = :titre
Sans écrire une seule ligne de JPQL !
findBy...
getBy...
readBy...
countBy...
existsBy...
deleteBy...
public interface LivreRepository extends JpaRepository<Livre, Long> { // --- Égalité simple --- // WHERE l.titre = :titre List<Livre> findByTitre(String titre); // --- LIKE --- // WHERE l.titre LIKE %:motCle% List<Livre> findByTitreContaining(String motCle); // WHERE l.titre LIKE :prefix% List<Livre> findByTitreStartingWith(String prefix); // WHERE l.titre LIKE %:suffix List<Livre> findByTitreEndingWith(String suffix); // --- Comparaisons numériques --- // WHERE l.prix < :prix List<Livre> findByPrixLessThan(BigDecimal prix); // WHERE l.prix <= :prix List<Livre> findByPrixLessThanEqual(BigDecimal prix); // WHERE l.prix > :prix List<Livre> findByPrixGreaterThan(BigDecimal prix); // WHERE l.prix BETWEEN :min AND :max List<Livre> findByPrixBetween(BigDecimal min, BigDecimal max); // --- NULL --- // WHERE l.categorie IS NULL List<Livre> findByCategorieIsNull(); // WHERE l.categorie IS NOT NULL List<Livre> findByCategorieIsNotNull(); // --- IN --- // WHERE l.anneePublication IN :annees List<Livre> findByAnneePublicationIn(List<Integer> annees); // --- AND / OR --- // WHERE l.categorie.id = :catId AND l.prix < :prix List<Livre> findByCategorieIdAndPrixLessThan(Long catId, BigDecimal prix); // WHERE a.nom = :nom OR a.prenom = :prenom List<Auteur> findByNomOrPrenom(String nom, String prenom); // --- Navigation dans les relations --- // WHERE l.auteur.nom = :nom (génère automatiquement le JOIN) List<Livre> findByAuteurNom(String nom); // WHERE l.auteur.nationalite = :nationalite List<Livre> findByAuteurNationalite(String nationalite); // --- Comptage --- // SELECT COUNT(l) WHERE l.categorie.libelle = :libelle Long countByCategorieLibelle(String libelle); // --- Existence --- // SELECT CASE WHEN COUNT(l) > 0 THEN TRUE ELSE FALSE END WHERE l.titre = :titre boolean existsByTitre(String titre); // --- Tri --- // ORDER BY l.titre ASC List<Livre> findByAuteurIdOrderByTitreAsc(Long auteurId); // ORDER BY l.prix DESC List<Livre> findByCategorieLibelleOrderByPrixDesc(String libelle); // --- Limite de résultats --- // SELECT TOP 1 ... (ou LIMIT 1 en PostgreSQL) Optional<Livre> findFirstByOrderByPrixDesc(); // Les 5 livres les moins chers List<Livre> findTop5ByOrderByPrixAsc(); // --- Distinct --- List<Livre> findDistinctByAuteurNationalite(String nationalite); }
And
AND
Or
OR
Is
Equals
=
Between
BETWEEN
LessThan
<
LessThanEqual
<=
GreaterThan
>
GreaterThanEqual
>=
IsNull
IS NULL
IsNotNull
NotNull
IS NOT NULL
Like
NotLike
NOT LIKE
StartingWith
LIKE 'x%'
EndingWith
LIKE '%x'
Containing
LIKE '%x%'
OrderBy
ORDER BY
Not
!=
In
IN
NotIn
NOT IN
True
= TRUE
False
= FALSE
IgnoreCase
UPPER(x) = UPPER(y)
Les méthodes dérivées sont très pratiques, mais elles ont leurs limites :
Dans ces cas, on utilise @Query.
public interface LivreRepository extends JpaRepository<Livre, Long> { // Requête JPQL simple @Query("SELECT l FROM Livre l WHERE l.titre LIKE %:motCle%") List<Livre> rechercherParMotCle(@Param("motCle") String motCle); // JOIN FETCH pour éviter N+1 @Query("SELECT DISTINCT l FROM Livre l JOIN FETCH l.auteur JOIN FETCH l.categorie") List<Livre> findAllAvecDetails(); // Requête avec agrégation @Query("SELECT l.categorie.libelle, COUNT(l), AVG(l.prix) " + "FROM Livre l " + "GROUP BY l.categorie.libelle " + "HAVING COUNT(l) > :minLivres " + "ORDER BY COUNT(l) DESC") List<Object[]> statistiquesParCategorie(@Param("minLivres") Long minLivres); // Sous-requête @Query("SELECT l FROM Livre l WHERE l.prix > " + "(SELECT AVG(l2.prix) FROM Livre l2 WHERE l2.categorie = l.categorie)") List<Livre> findLivresPlusChersQueMoyenneCategorie(); // Paramètre de type entité @Query("SELECT l FROM Livre l WHERE l.auteur = :auteur ORDER BY l.anneePublication DESC") List<Livre> findByAuteurOrderByDate(@Param("auteur") Auteur auteur); // Expression CASE @Query("SELECT l.titre, " + "CASE WHEN l.stock = 0 THEN 'Épuisé' " + " WHEN l.stock < 5 THEN 'Faible stock' " + " ELSE 'Disponible' END " + "FROM Livre l") List<Object[]> findLivresAvecStatutStock(); }
Parfois, on a besoin de fonctions spécifiques à PostgreSQL que JPQL ne supporte pas.
public interface LivreRepository extends JpaRepository<Livre, Long> { // Utilisation de ILIKE (insensible à la casse, spécifique PostgreSQL) @Query(value = "SELECT * FROM livres WHERE titre ILIKE %:motCle%", nativeQuery = true) List<Livre> rechercherInsensibleCasse(@Param("motCle") String motCle); // Fonction window PostgreSQL (non disponible en JPQL) @Query(value = "SELECT *, RANK() OVER (PARTITION BY categorie_id ORDER BY prix DESC) AS rang " + "FROM livres", nativeQuery = true) List<Object[]> findLivresAvecRangParCategorie(); // Full-text search PostgreSQL @Query(value = "SELECT * FROM livres " + "WHERE to_tsvector('french', titre) @@ plainto_tsquery('french', :recherche)", nativeQuery = true) List<Livre> fullTextSearch(@Param("recherche") String recherche); }
Avec nativeQuery = true, on perd la portabilité entre bases de données. Utilisez-le seulement lorsque c’est vraiment nécessaire !
nativeQuery = true
Pour les UPDATE et DELETE, on ajoute @Modifying et on exécute dans une transaction.
@Modifying
public interface LivreRepository extends JpaRepository<Livre, Long> { // Mise à jour du stock @Modifying @Transactional @Query("UPDATE Livre l SET l.stock = :stock WHERE l.id = :id") int updateStock(@Param("id") Long id, @Param("stock") Integer stock); // Suppression par catégorie @Modifying @Transactional @Query("DELETE FROM Livre l WHERE l.categorie.id = :categorieId AND l.stock = 0") int deleteEpuisesParCategorie(@Param("categorieId") Long categorieId); // Augmentation de prix par catégorie @Modifying @Transactional @Query("UPDATE Livre l SET l.prix = l.prix * :facteur " + "WHERE l.categorie.libelle = :categorie") int augmenterPrixParCategorie( @Param("facteur") BigDecimal facteur, @Param("categorie") String categorie ); }
@Modifying indique à Spring Data que c’est une requête de modification. @Transactional est nécessaire sur la méthode du repository (ou du service qui l’appelle).
@Transactional
Charger une entité complète quand on n’a besoin que de quelques champs est inefficace :
Les projections permettent de ne récupérer que les champs nécessaires. C’est une bonne pratique de ne récupérer que ce dont nous avons besoin.
On définit une interface dont les méthodes correspondent aux champs qu’on veut récupérer. Spring Data JPA crée automatiquement une implémentation à la volée.
// Projection simple : seulement titre et prix public interface LivreResume { String getTitre(); BigDecimal getPrix(); } // Projection avec navigation dans les relations (Open Projection) public interface LivreAvecAuteur { String getTitre(); BigDecimal getPrix(); // Accès imbriqué : Spring Data crée automatiquement le JOIN AuteurResume getAuteur(); interface AuteurResume { String getNom(); String getPrenom(); } }
Utilisation dans le Repository
public interface LivreRepository extends JpaRepository<Livre, Long> { // Spring Data sait que le retour est une projection List<LivreResume> findByCategoriLibelle(String libelle); // Projection avec relation List<LivreAvecAuteur> findByCategorieId(Long categorieId); // Méthode générique avec projection (très puissant !) <T> List<T> findByPrixLessThan(BigDecimal prix, Class<T> type); }
La méthode générique s’utilise ainsi :
// On décide du type de projection au moment de l'appel List<LivreResume> resumes = livreRepository.findByPrixLessThan( new BigDecimal("20"), LivreResume.class); List<Livre> entites = livreRepository.findByPrixLessThan( new BigDecimal("20"), Livre.class);
Une autre approche est le constructeur JPQL : on crée un DTO avec un constructeur et on l’instancie directement dans la requête.
// Le DTO : classe simple avec un constructeur public class LivreDTO { private final String titre; private final BigDecimal prix; private final String nomAuteur; // Ce constructeur DOIT correspondre exactement aux arguments du NEW dans JPQL public LivreDTO(String titre, BigDecimal prix, String nomAuteur) { this.titre = titre; this.prix = prix; this.nomAuteur = nomAuteur; } // getters... }
Requête JPQL avec constructeur :
// avec EntityManager directement List<LivreDTO> dtos = em.createQuery( "SELECT NEW com.exemple.dto.LivreDTO(l.titre, l.prix, CONCAT(a.prenom, ' ', a.nom)) " + "FROM Livre l JOIN l.auteur a " + "WHERE l.categorie.libelle = :categorie", LivreDTO.class ) .setParameter("categorie", "Roman") .getResultList(); // Avec Spring Data JPA (@Query) public interface LivreRepository extends JpaRepository<Livre, Long> { @Query("SELECT NEW com.exemple.dto.LivreDTO(l.titre, l.prix, CONCAT(a.prenom, ' ', a.nom)) " + "FROM Livre l JOIN l.auteur a WHERE l.categorie.libelle = :categorie") List<LivreDTO> findLivreDTOByCategorie(@Param("categorie") String categorie); }
Conseil : Depuis Java 14, utilisez des records comme DTOs : syntaxe beaucoup plus concise !
// Avec un record Java (Java 14+) public record LivreDTO(String titre, BigDecimal prix, String nomAuteur) {}
Les interfaces de projection peuvent contenir des expressions SpEL pour calculer des valeurs à partir des données de l’entité.
public interface LivreProjectionAvancee { String getTitre(); BigDecimal getPrix(); // Expression SpEL : concaténation côté Java (pas en JPQL) @Value("#{target.auteur.prenom + ' ' + target.auteur.nom}") String getNomCompletAuteur(); // Calcul SpEL @Value("#{target.prix * 1.20}") // Prix TTC avec TVA 20% BigDecimal getPrixTTC(); }
Les @Value SpEL sont calculés côté Java, donc l’entité complète (avec ses relations) doit être chargée. Préférez les colonnes calculées directement en JPQL pour les performances.
@Value
Quand une table contient des milliers de lignes, retourner tout d’un coup est catastrophique pour les performances (mémoire, réseau, temps de réponse). La pagination permet de récupérer les données par pages en déterminant le nombre des éléments.
Spring Data JPA fournit une interface Pageable et son implémentation PageRequest. Comme vous le savez maintenant, on aime bien les interfaces en Java !
Pageable
PageRequest
// Créer un objet Pageable : page 0 (première page), 10 éléments par page Pageable premierePage = PageRequest.of(0, 10); // Page 2 (troisième page), 20 éléments, triés par titre Pageable pageTitre = PageRequest.of(2, 20, Sort.by("titre")); // Tri multi-colonnes Pageable pageComplexe = PageRequest.of(0, 15, Sort.by(Sort.Direction.DESC, "prix") .and(Sort.by(Sort.Direction.ASC, "titre")));
public interface LivreRepository extends JpaRepository<Livre, Long> { // Retourne une Page<T> (avec métadonnées de pagination) Page<Livre> findByCategorieLibelle(String libelle, Pageable pageable); // Retourne une Slice<T> (sait s'il y a une page suivante, plus léger) Slice<Livre> findByAuteurNationalite(String nationalite, Pageable pageable); // Retourne une List<T> simple (pas de count automatique) List<Livre> findByPrixLessThan(BigDecimal prix, Pageable pageable); // Avec @Query @Query("SELECT l FROM Livre l WHERE l.stock > 0 ORDER BY l.prix ASC") Page<Livre> findLivresDisponibles(Pageable pageable); }
L’objet Page<T> contient les données ET les métadonnées de pagination :
Page<T>
@Service public class LivreService { public void afficherPageLivres(String categorie, int pageNum, int pageSize) { Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by("prix")); Page<Livre> page = livreRepository.findByCategorieLibelle(categorie, pageable); // Les données de la page courante List<Livre> livres = page.getContent(); // Métadonnées System.out.println("Page " + page.getNumber() + " sur " + page.getTotalPages()); System.out.println("Livres sur cette page : " + page.getNumberOfElements()); System.out.println("Total de livres : " + page.getTotalElements()); System.out.println("Première page ? " + page.isFirst()); System.out.println("Dernière page ? " + page.isLast()); System.out.println("Page suivante disponible ? " + page.hasNext()); } }
public interface LivreRepository extends JpaRepository<Livre, Long> { // La méthode accepte un Sort en plus des paramètres habituels List<Livre> findByCategorie(Categorie categorie, Sort sort); } // Utilisation Sort parPrix = Sort.by(Sort.Direction.ASC, "prix"); Sort parTitrePuisPrix = Sort.by("titre").and(Sort.by("prix")); Sort ignoreCase = Sort.by(Sort.Order.by("titre").ignoreCase()); List<Livre> livres = livreRepository.findByCategorie(categorie, parPrix);
public interface LivreRepository extends JpaRepository<Livre, Long> { // Spring Data injecte automatiquement la pagination dans la requête JPQL @Query("SELECT l FROM Livre l WHERE l.prix BETWEEN :min AND :max") Page<Livre> findByPrixEntre( @Param("min") BigDecimal min, @Param("max") BigDecimal max, Pageable pageable ); // Pour les requêtes COUNT complexes, on peut spécifier une countQuery séparée @Query( value = "SELECT l FROM Livre l JOIN FETCH l.auteur WHERE l.stock > 0", countQuery = "SELECT COUNT(l) FROM Livre l WHERE l.stock > 0" ) Page<Livre> findDisponiblesAvecAuteur(Pageable pageable); }
La countQuery séparée est importante quand la requête principale contient un JOIN FETCH : Hibernate ne peut pas faire un COUNT sur une requête avec FETCH JOIN.
countQuery
La Criteria API est une façon de construire des requêtes JPA de manière programmatique (en Java pur), sans écrire de chaînes de caractères JPQL. Elle est particulièrement utile pour construire des requêtes dynamiques dont les conditions varient selon les paramètres.
// Exemple : recherche avec filtres optionnels public class LivreSearchCriteria { private String titre; // optionnel private String categorieNom; // optionnel private BigDecimal prixMin; // optionnel private BigDecimal prixMax; // optionnel private Integer anneeMin; // optionnel }
@Repository public class LivreSearchRepository { @PersistenceContext private EntityManager em; public List<Livre> rechercher(LivreSearchCriteria criteres) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Livre> cq = cb.createQuery(Livre.class); Root<Livre> livre = cq.from(Livre.class); // On construit la liste des conditions dynamiquement List<Predicate> predicats = new ArrayList<>(); if (criteres.getTitre() != null && !criteres.getTitre().isBlank()) { predicats.add(cb.like( cb.lower(livre.get("titre")), "%" + criteres.getTitre().toLowerCase() + "%" )); } if (criteres.getCategorieNom() != null) { Join<Livre, Categorie> categorieJoin = livre.join("categorie", JoinType.LEFT); predicats.add(cb.equal( categorieJoin.get("libelle"), criteres.getCategorieNom() )); } if (criteres.getPrixMin() != null) { predicats.add(cb.greaterThanOrEqualTo(livre.get("prix"), criteres.getPrixMin())); } if (criteres.getPrixMax() != null) { predicats.add(cb.lessThanOrEqualTo(livre.get("prix"), criteres.getPrixMax())); } // Combiner toutes les conditions avec AND cq.where(cb.and(predicats.toArray(new Predicate[0]))); cq.orderBy(cb.asc(livre.get("titre"))); return em.createQuery(cq).getResultList(); } }
Spring Data JPA encapsule la Criteria API dans des Specification<T>, qui sont plus lisibles et composables.
Specification<T>
// Activer les Specifications dans le repository public interface LivreRepository extends JpaRepository<Livre, Long>, JpaSpecificationExecutor<Livre> { // Pas besoin d'ajouter de méthodes ici } // Définir les Specifications (des prédicats réutilisables) public class LivreSpecifications { public static Specification<Livre> titreLike(String motCle) { return (root, query, cb) -> motCle == null ? null : cb.like(cb.lower(root.get("titre")), "%" + motCle.toLowerCase() + "%"); } public static Specification<Livre> prixMax(BigDecimal max) { return (root, query, cb) -> max == null ? null : cb.lessThanOrEqualTo(root.get("prix"), max); } public static Specification<Livre> categorieEgale(String categorie) { return (root, query, cb) -> categorie == null ? null : cb.equal(root.join("categorie").get("libelle"), categorie); } } // Utilisation : combinaison dynamique des Specifications @Service public class LivreService { public List<Livre> rechercher(String motCle, BigDecimal prixMax, String categorie) { Specification<Livre> spec = Specification .where(LivreSpecifications.titreLike(motCle)) .and(LivreSpecifications.prixMax(prixMax)) .and(LivreSpecifications.categorieEgale(categorie)); return livreRepository.findAll(spec); } }
Lorsque l’on retourne du JSON, on a souvent des boucles du fait des relations bidirectionnelles entre certaines entités y compris pour des requêtes JPQL.
Récapitulatif des solutions :
// 1. JOIN FETCH dans une @Query @Query("SELECT DISTINCT l FROM Livre l JOIN FETCH l.auteur") List<Livre> findAllAvecAuteur(); // 2. @EntityGraph : déclarer les associations à charger "eagerly" sur une requête @EntityGraph(attributePaths = {"auteur", "categorie"}) List<Livre> findByCategorieId(Long id); // 3. @BatchSize sur l'entité (charge les relations en lots, pas une par une) @OneToMany(mappedBy = "auteur", fetch = FetchType.LAZY) @BatchSize(size = 25) // charge 25 collections à la fois au lieu d'1, on choisit ! private List<Livre> livres;
# application.properties spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Encore plus détaillé (avec les valeurs des paramètres) logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
@Service @Transactional(readOnly = true) // Par défaut : lecture seule (optimisation) public class LivreService { public List<Livre> tousLesLivres() { return livreRepository.findAll(); // Pas de gestion de transaction spéciale } @Transactional // Override : cette méthode peut écrire public Livre creer(Livre livre) { return livreRepository.save(livre); } @Transactional // Override public void supprimer(Long id) { livreRepository.deleteById(id); } }
@Entity @Table(name = "livres", indexes = { @Index(name = "idx_livres_titre", columnList = "titre"), @Index(name = "idx_livres_auteur_id", columnList = "auteur_id"), @Index(name = "idx_livres_prix", columnList = "prix") }) public class Livre { // ... }
// PIÈGE 1 : Charger des entités pour juste les supprimer livreRepository.findAll().forEach(l -> livreRepository.delete(l)); // TRÈS MAUVAIS // SOLUTION : deleteAll() ou bulk delete livreRepository.deleteAll(); // ou deleteAllInBatch() pour aller encore plus vite // PIÈGE 2 : Oublier @Transactional sur les opérations d'écriture public Livre mettreAJour(Livre livre) { return livreRepository.save(livre); // Fonctionne hors transaction mais dangereux } // SOLUTION @Transactional public Livre mettreAJour(Livre livre) { return livreRepository.save(livre); } // PIÈGE 3 : LazyInitializationException // Accéder à une collection LAZY en dehors d'une session Hibernate public List<Livre> getLivresAuteur(Long auteurId) { Auteur auteur = auteurRepository.findById(auteurId).get(); return auteur.getLivres(); // Gén-re une Exception si la session est fermée ! } // SOLUTION : JOIN FETCH ou appel dans une méthode @Transactional @Transactional(readOnly = true) public List<Livre> getLivresAuteur(Long auteurId) { Auteur auteur = auteurRepository.findById(auteurId).get(); return auteur.getLivres(); // OK : session encore ouverte dans @Transactional }
L’énoncé complet du TP est fourni.
Lien du TP complet
Voici un avant-goût des objectifs :
Vous allez construire EcoTrack, une application de suivi des actions écologiques des utilisateurs. L’application permettra de :
Technologies : Spring Boot 3 ou 4, Spring Data JPA, Hibernate, PostgreSQL, Maven