🎯 OBJECTIF
Catalogue condensé des 20 pièges Hibernate/JPA les plus fréquents en production : symptômes, causes, et solutions. À utiliser comme checklist de review de code et de diagnostic de performances.
🧠 MODÈLE MENTAL
La plupart des problèmes Hibernate en production ne viennent pas de bugs Hibernate — ils viennent d'une méconnaissance du mapping ou d'hypothèses incorrectes sur ce que JPA fait automatiquement. Hibernate fait exactement ce que le mapping lui ordonne. Les "surprises" sont des comportements spec-conformes qu'on n'a pas anticipés.
Ce catalogue est organisé par famille : problèmes de chargement (N+1, EAGER, OSIV), problèmes de collections (BAG, orphanRemoval), problèmes d'écriture (bulk, batching, @Version), problèmes de transactions (durée, appels externes), et problèmes de modèle (equals/hashCode, indexes). Chaque piège a un symptôme visible et une correction directe.
Piège 1 — OSIV activé
spring.jpa.open-in-view=true (défaut) garde la session ouverte jusqu'à la fin de la requête HTTPspring.jpa.open-in-view=false + construire les DTO dans la transaction avec fetch contrôléPiège 2 — JOIN FETCH sur plusieurs collections
MultipleBagFetchException@OneToMany → explosion cartésienneIN)Piège 3 — Pagination + JOIN FETCH collection
HHH90003004, pages incohérentes, très mauvaises performancesPageable + JOIN FETCH sur collection → pagination en mémoireIN + JOIN FETCHPiège 4 — fetch EAGER partout
@OneToMany(fetch = EAGER) — chargement automatique même quand inutilePiège 5 — N+1 classique sur relations LAZY
@BatchSize(size=25) / default_batch_fetch_size, ou EntityGraph, ou JOIN FETCHPiège 6 — List sans @OrderColumn (BAG)
Set si pas d'ordre métier, List + @OrderColumn si ordre nécessaire, ne jamais remplacer la collection entièrePiège 7 — orphanRemoval + remplacement de collection
entity.setCollection(new ArrayList<>()) avec orphanRemoval=true → tous les enfants deviennent orphelinsadd()/remove()), jamais remplacer l'instancePiège 8 — cascade = ALL partout
CascadeType.ALL sur des relations partagées propage REMOVE/MERGE sans contrôlePERSIST seul dans la plupart des cas, REMOVE uniquement pour les agrégats fortsPiège 9 — Mauvais equals/hashCode
remove() inefficace dans les Sets, duplicats, DELETE + INSERT inattendus au lieu d'UPDATEequals()/hashCode() basés sur champs mutables ou collectionsPiège 10 — save() avec ID déjà défini
merge() → SELECT pour vérifier l'existencepersist() pour les vrais inserts, save() uniquement sur entités MANAGEDPiège 11 — JDBC batching inactif sans le savoir
batch_size configuréIDENTITY, statements non identiques, ou flush trop fréquentSEQUENCE + order_inserts=true + order_updates=true + persist() + flush()/clear() par lotsPiège 12 — Bulk UPDATE/DELETE sans flush/clear
@Modifying bypass Hibernate → L1 désynchroniséflushAutomatically=true avant le bulk + clearAutomatically=true aprèsPiège 13 — @Version + batching non configuré
batch_versioned_data=false (défaut) désactive le batching sur entités versionnéeshibernate.batch_versioned_data=truePiège 14 — Flush surprise (FlushMode AUTO)
FlushMode.COMMIT sur les queries cibléesPiège 15 — Transactions trop longues
OptimisticLockException, timeouts, contention élevée@TransactionalREQUIRES_NEW cibléPiège 16 — Hot row (ligne très concurrente)
OptimisticLockException en boucle sur la même lignePiège 17 — Appels externes dans la transaction
@Transactional → transaction ouverte pendant l'appel externeApplicationEventPublisher avec after-commit, async hors transactionPiège 18 — Persistence Context trop rempli
clear() — L1 conserve toutflush() + clear() réguliers (tous les N=batch_size)Piège 19 — Mauvais indexes DB
EXPLAIN ANALYZEEXPLAIN ANALYZE sur les requêtes lentes, index composites adaptés à l'ordre des clauses WHEREPiège 20 — @Lob / gros champs inutilement chargés
@Lob ou TEXT chargé systématiquement même quand inutile⚡ TL;DR — les 5 pièges les plus coûteux en prod
OSIV (Piège 1)
✓ Désactiver spring.jpa.open-in-view et construire les DTO dans la transaction.
⚠ Activé par défaut dans Spring Boot — source n°1 de N+1 invisibles en production.
List BAG (Piège 6)
✓ Utiliser Set pour toutes les collections @OneToMany sauf si l'ordre est métier.
⚠ Un seul ajout à une List de 500 éléments = 501 DELETE + 501 INSERT.
IDENTITY + batching (Piège 11)
✓ SEQUENCE + order_inserts=true + persist() + flush()/clear() par lots.
⚠ IDENTITY désactive silencieusement le batching — aucun log, aucun avertissement.
Appels externes en transaction (Piège 17)
✓ Outbox pattern ou after-commit pour les events — jamais d'appel HTTP/Kafka dans @Transactional.
⚠ Un appel externe lent dans une transaction = lock prolongé sur les tables modifiées.
Persistence Context gonfle (Piège 18)
✓ flush() + clear() tous les N éléments dans les traitements batch.
⚠ saveAll() sur 100 000 entités sans flush intermédiaire → OOM avant le premier commit.
🎓 À retenir
spring.jpa.open-in-view=true dans Spring Boot. Sur la majorité des applications de production, le désactiver immédiatement et construire les DTOs dans la couche service/transaction.HHH90003004 est une alarme silencieuse — Hibernate affiche ce warning dans les logs quand il pagine en mémoire à cause de JOIN FETCH + setMaxResults. L'application continue de fonctionner mais charge potentiellement des milliers de lignes en RAM. Chercher ce pattern dans les logs de CI.EXPLAIN ANALYZE avant d'optimiser le code JPA — avant de changer le mapping ou d'ajouter du cache, vérifier le plan d'exécution SQL. Un index manquant peut être la vraie cause d'une lenteur qu'on attribue à tort à Hibernate.