Hibernate — Mélanger les modes d'accès dans un même traitement
1. Modes d'accès à la base de données
Avant d'aborder les problèmes, rappelons les modes disponibles :
Mode
API
Passe par le PC / Cache L1 ?
Dirty checking ?
Cascade ?
Session ORM
session.save/update/delete/get
✅ Oui
✅ Oui
✅ Oui
HQL/JPQL SELECT
session.createQuery(...).list()
✅ Oui — entités attachées
✅ Oui
✅ Oui
HQL/JPQL Bulk
createQuery("UPDATE...").executeUpdate()
❌ Bypass total
❌ Non
❌ Non
SQL natif
session.createNativeQuery(...)
❌ Bypass total
❌ Non
❌ Non
StatelessSession
statelessSession.get/update/insert
❌ Aucun PC
❌ Non
❌ Non
2. Exemple problématique
3. Ce qui se passe RÉELLEMENT
4. Les problèmes détaillés
Problème 1 — Le bulk HQL/SQL bypasse le cache L1 (PC)
C'est ce que la documentation officielle Hibernate énonce explicitement :
"In keeping with the EJB3 specification, HQL UPDATE statements, by default, do not affect the version or the timestamp property values for the affected entities."
Et le Dev Guide Hibernate le confirme dans le chapitre Batch Processing : un executeUpdate() envoie le SQL directement en base, sans passer par le Persistence Context. Les entités déjà présentes dans le PC ne sont pas rafraîchies.
Problème 2 — Le cache L1 retourne des données périmées silencieusement
Après un bulk UPDATE, un session.find(id) dans la même session retourne l'entité du cache L1 — sans aller en base — avec les anciennes valeurs. Hibernate n'a aucun moyen de savoir que la base a changé sous ses pieds. Aucune exception n'est levée, le bug est totalement silencieux.[^2][^3]
Problème 3 — Le flush écrase les modifications bulk
C'est le plus dangereux. Si une entité est chargée dans le PC avant un bulk UPDATE, puis que la session est flushée après, le dirty checking Hibernate repart des valeurs périmées du PC et émet un UPDATE qui réécrase les modifications du bulk. Les données bulk sont perdues sans aucun avertissement.
Problème 4 — SQL natif invalide le cache L2 entièrement
Un createNativeQuery("UPDATE...") invalide tout le cache L2 car Hibernate ne sait pas quelles entités ont été affectées :
"Hibernate invalidates the 2nd level cache if you execute an SQL UPDATE or DELETE statement as a native query. By default, Hibernate doesn't know which records were affected. Due to this, Hibernate can only invalidate the entire 2nd level cache."
Sur JBoss EAP avec Infinispan, ce comportement a même évolué entre les versions, générant des données périmées en cache après un JPAQL bulk.
Problème 5 — UPDATE versioned et incohérence de version
Le bulk HQL UPDATEn'incrémente pas la version par défaut. Si des entités avec @Version sont dans le PC et qu'un bulk les modifie, la version en base et la version en mémoire divergent → OptimisticLockException au flush. Le mot-clé VERSIONED existe mais a des limitations (incompatible avec les UserVersionType).
5. A ne pas faire
6. Les bonnes pratiques officielles Hibernate
Ne jamais mélanger bulk et Session ORM dans la même session
La documentation Hibernate Dev Guide est formelle : les opérations bulk (executeUpdate()) et les opérations via Session doivent être séparées dans des sessions distinctes, ou le PC doit être explicitement nettoyé après chaque bulk.
// ✅ CORRECT : session dédiée au bulkSessionbulkSession=sessionFactory.openSession();bulkSession.createQuery("UPDATE Personne SET status=:s WHERE ...").executeUpdate();bulkSession.flush();bulkSession.close();// Session propre pour la suiteSessionreadSession=sessionFactory.openSession();PersonnePersonne=readSession.find(Personne.class,id);// Données fraîches garanties
session.clear() obligatoire après un bulk dans la même session
Si le mélange est inévitable dans la même session, il faut vider le PC après le bulk pour forcer le rechargement depuis la base :
// ⚠️ Acceptable si inévitablesession.createQuery("UPDATE Personne SET status='QUALIFIE' WHERE ...").executeUpdate();session.flush();session.clear();// ← OBLIGATOIRE — vide le PC, force rechargement depuis DBPersonnePersonne=session.find(Personne.class,id);// Maintenant frais ✅
SQL natif : déclarer les tables synchronisées
Pour éviter l'invalidation totale du cache L2 lors d'un SQL natif, déclarer les entités impactées :
session.createNativeQuery("UPDATE Personne SET locked=1 WHERE id=:id").unwrap(NativeQuery.class).addSynchronizedEntityClass(Personne.class)// ← Hibernate sait quelle région invalider.executeUpdate();
UPDATE VERSIONED si entités versionnées impliquées
// Force l'incrémentation de @Version lors d'un bulk HQLsession.createQuery("UPDATE VERSIONED Personne SET status=:s WHERE status=:old").executeUpdate();