🎯 OBJECTIF
Comprendre comment :
🧠 MODÈLE MENTAL
JPA est un contrat — une spécification qui définit comment les objets Java doivent être mappés à une base relationnelle. Mais un contrat seul ne fait rien : il faut un artisan. Hibernate est cet artisan. Il respecte le contrat JPA, mais c'est lui qui fait le travail réel : gérer le cache de premier niveau, générer le SQL dialecte par dialecte, tracker les entités chargées.
JPQL est la langue dans laquelle tu donnes tes instructions à cet artisan : orientée entités Java, pas de tables SQL. Tu parles Order, Customer, status — Hibernate traduit ça en SELECT o.* FROM orders o WHERE o.status = ?. Ce découplage te permet de changer de base sans réécrire tes requêtes.
Spring Data JPA est la couche du dessus : elle génère le JPQL depuis les noms de méthodes (findByStatusAndCustomerId) ou les annotations @Query. Tu n'écris presque plus de requêtes — jusqu'au jour où tu atteins les limites de JPQL et tu dois descendre en SQL natif.
flowchart LR
A[Spring Data JPA\nRepository] --> B[JPQL / @Query]
B --> C[Hibernate\nORM Engine]
C --> D[SQL natif\ndialecte-spécifique]
D --> E[(Base de données\nPostgreSQL / MySQL...)]mermaid| Couche | Rôle | Ce qu'elle produit |
|---|---|---|
| Spring Data JPA | Génère JPQL depuis les noms de méthodes | Requête JPQL |
| JPA (spec) | Définit le contrat EntityManager, annotations | Interface standardisée |
| Hibernate | Traduit JPQL → SQL, gère le cache L1, les entités | SQL dialecte |
| JDBC | Envoie le SQL à la base, récupère les ResultSet | Rows bruts |
| Base de données | Exécute et retourne les données | Données |
🔑 Conclusion clé
Chaque couche ajoute une abstraction utile — mais aussi un niveau de complexité. Quand quelque chose ne va pas (mauvaise requête, N+1), il faut savoir descendre dans la pile pour diagnostiquer.
Les annotations @Entity, @OneToMany, @Column viennent de JPA (package jakarta.persistence). Mais le comportement réel — comment le lazy loading se déclenche, quand le cache L1 invalide une entité — c'est Hibernate qui l'implémente.
@Entity // ✓ annotation JPA standard
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // ✓ contrat JPA
@JoinColumn(name = "customer_id")
private Customer customer; // ⚠️ Hibernate charge en proxy — pas encore une vraie Customer
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderLine> lines = new ArrayList<>();
}java🔑 Conclusion clé
Les annotations sont JPA, le comportement est Hibernate. Chercher pourquoi le lazy loading "ne marche pas" dans la Javadoc JPA est une perte de temps — c'est une décision d'implémentation Hibernate.
JPQL couvre 90% des besoins quotidiens. Il atteint ses limites dès qu'on touche à des fonctionnalités SQL avancées.
// ✓ Spring Data génère ce JPQL depuis le nom de méthode
List<Order> findByStatusAndCustomerId(String status, Long customerId);
// ✓ JPQL explicite avec @Query — toujours sur entités, jamais sur tables
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
List<Order> findRecentByStatus(@Param("status") String status);java// ✓ SQL natif — nécessaire pour les window functions (non supportées en JPQL)
@Query(value = """
SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders
WHERE status = :status
""", nativeQuery = true)
List<Object[]> findOrdersWithRank(@Param("status") String status);java| Critère | JPQL | SQL natif (nativeQuery = true) |
|---|---|---|
| Portabilité | ✅ Tous dialectes | ❌ Lié au SGBD cible |
| Window functions | ❌ Non supporté | ✅ Supporté |
RETURNING (PG) |
❌ Non supporté | ✅ Supporté |
COPY / bulk import |
❌ Non supporté | ✅ Via JDBC direct |
| Mapping entités | ✅ Automatique | ⚠️ Manuel ou @SqlResultSetMapping |
Règle de décision
Commencer par JPQL. Passer à nativeQuery = true dès que la fonctionnalité SQL n'existe pas en JPQL. Ce n'est pas un aveu d'échec — c'est utiliser le bon outil au bon niveau d'abstraction.
🚨 Piège N+1
@OneToMany sans JOIN FETCH ou @EntityGraph dans une boucle → N requêtes SQL pour N entités. JPQL ne protège pas contre ça. Toujours vérifier les logs SQL en dev avec org.hibernate.SQL: DEBUG.
logging:
level:
org.hibernate.SQL: DEBUG # ✓ affiche les requêtes SQL générées
org.hibernate.orm.jdbc.bind: TRACE # ✓ affiche les paramètres bindésyamlSans org.hibernate.orm.jdbc.bind: TRACE, les ? sont visibles mais pas les valeurs — rend le diagnostic de requêtes incorrectes quasi impossible en dev.
⚡ TL;DR — chaque concept en une ligne
JPA ✓ Spécification Java standard qui définit le contrat ORM : annotations, cycle de vie des entités, API EntityManager. ⚠ JPA ne fait rien seul — sans implémentation (Hibernate), aucune ligne SQL ne s'exécute.
Hibernate ✓ L'implémentation de JPA la plus répandue : gère le cache L1, génère le SQL adapté au dialecte, traduit les entités en lignes. ⚠ Hibernate a ses propres extensions au-delà de JPA — les utiliser crée un couplage que l'abstraction JPA était censée éviter.
JPQL ✓ Langage de requête orienté entités (pas de tables) : portable entre bases, intégré nativement dans JPA. ⚠ JPQL ne supporte pas les fonctions avancées SQL (window functions, RETURNING, COPY) — il faut alors descendre en SQL natif.
Spring Data JPA ✓ Génère JPQL depuis les noms de méthodes et fournit les repositories : zéro boilerplate pour les requêtes CRUD courantes. ⚠ L'abstraction peut masquer des requêtes N+1 ou des jointures inefficaces — toujours vérifier le SQL généré en développement.
🎓 À retenir
@OneToMany est dans la spec, mais comment Hibernate charge le proxy et quand il flush en base, c'est son implémentation. Chercher dans la Javadoc JPA pourquoi le lazy loading se comporte bizarrement est une perte de temps.nativeQuery = true n'est pas un hack — c'est la réponse correcte dès que JPQL ne couvre pas le besoin. La dépasser délibérément est une décision d'architecture, pas un contournement.org.hibernate.orm.jdbc.bind: TRACE est la clé manquante — org.hibernate.SQL: DEBUG montre la structure de la requête avec des ?. Sans le niveau bind, les valeurs des paramètres restent invisibles et le diagnostic devient impossible.