🎬 Passer en mode présentation

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

Mix Session ORM + Bulk + SQL natifMix Session ORM + Bulk + SQL natifD.veloppeurSession HibernateDBDéveloppeurDéveloppeurSession Hibernate(PC actif)Session Hibernate(PC actif)DBDB1. find(Personne.class, id)→ entité [version=1,status=VALIDE]Tous les accès sont synchronisés entre eux.Hibernate gère la cohérence.2. createQuery("UPDATE Personne SET status='QUALIFIE'WHERE status='VALIDE'").executeUpdate()Le bulk met à jour la DBET les entités déjà chargées dans le PC sont actualisées.3. session.get(Personne.class, id)On a status='QUALIFIE' ✅4. createNativeQuery("UPDATE Personne SET locked=1WHERE id=:id").executeUpdate()La session est à locked=1 maintenant.5. flush() + commit()Tout est cohérent ✅
Mix Session ORM + Bulk + SQL natifMix Session ORM + Bulk + SQL natifD.veloppeurSession HibernateDBDéveloppeurDéveloppeurSession Hibernate(PC actif)Session Hibernate(PC actif)DBDB1. find(Personne.class, id)→ entité [version=1,status=VALIDE]Tous les accès sont synchronisés entre eux.Hibernate gère la cohérence.2. createQuery("UPDATE Personne SET status='QUALIFIE'WHERE status='VALIDE'").executeUpdate()Le bulk met à jour la DBET les entités déjà chargées dans le PC sont actualisées.3. session.get(Personne.class, id)On a status='QUALIFIE' ✅4. createNativeQuery("UPDATE Personne SET locked=1WHERE id=:id").executeUpdate()La session est à locked=1 maintenant.5. flush() + commit()Tout est cohérent ✅

3. Ce qui se passe RÉELLEMENT

Ce qui se passe RÉELLEMENT — Le PC devient une source de données périméesCe qui se passe RÉELLEMENT — Le PC devient une source de données périméesD.veloppeurSession HibernatePC . cache L1DBDéveloppeurDéveloppeurSession HibernatePC = cache L1Session HibernatePC = cache L1DBDB1. find(Personne.class, id)SELECT Personne WHERE id=1status=VALIDE, locked=0, version=1PC contient :Personne#1 → status=VALIDE,locked=0, version=12. HQL Bulk UPDATE createQuery("UPDATE Personne SETstatus='QUALIFIE'...").executeUpdate()UPDATE Personne SET status='QUALIFIE'⚡ Exécution SQL IMMÉDIATE⚠️ Le bulk BYPASSE le PC ! La DB est mise à jour directement.Mais le PC garde en mémoire : Personne#1 → status=VALIDE ← PÉRIMÉ !3. find(Personne.class, id) (même session, même PC)⚠️ Hibernate retourne l'entité du CACHE L1 sans aller en DB !→ status=VALIDE retournéalors que DB = QUALIFIE DONNÉES PÉRIMÉES SILENCIEUSES !Personne.status = VALIDE ❌4. SQL natif UPDATE SET locked=1 WHERE id=1⚠️ SQL natif bypasse également le PC ET le cache L2.PC garde locked=0 en mémoire.5. flush() + commit()UPDATE Personne SET status=VALIDE, locked=0, version=2(dirty checking sur l'entité en mémoire !)❌ ÉCRASEMENT SILENCIEUX !Le flush repart des valeurspérimées du PC :- status QUALIFIE → VALIDE (rollback logique !)- locked=1 → 0 (rollback logique !)Pas d'exception levée. Les données bulk sont perdues.
Ce qui se passe RÉELLEMENT — Le PC devient une source de données périméesCe qui se passe RÉELLEMENT — Le PC devient une source de données périméesD.veloppeurSession HibernatePC . cache L1DBDéveloppeurDéveloppeurSession HibernatePC = cache L1Session HibernatePC = cache L1DBDB1. find(Personne.class, id)SELECT Personne WHERE id=1status=VALIDE, locked=0, version=1PC contient :Personne#1 → status=VALIDE,locked=0, version=12. HQL Bulk UPDATE createQuery("UPDATE Personne SETstatus='QUALIFIE'...").executeUpdate()UPDATE Personne SET status='QUALIFIE'⚡ Exécution SQL IMMÉDIATE⚠️ Le bulk BYPASSE le PC ! La DB est mise à jour directement.Mais le PC garde en mémoire : Personne#1 → status=VALIDE ← PÉRIMÉ !3. find(Personne.class, id) (même session, même PC)⚠️ Hibernate retourne l'entité du CACHE L1 sans aller en DB !→ status=VALIDE retournéalors que DB = QUALIFIE DONNÉES PÉRIMÉES SILENCIEUSES !Personne.status = VALIDE ❌4. SQL natif UPDATE SET locked=1 WHERE id=1⚠️ SQL natif bypasse également le PC ET le cache L2.PC garde locked=0 en mémoire.5. flush() + commit()UPDATE Personne SET status=VALIDE, locked=0, version=2(dirty checking sur l'entité en mémoire !)❌ ÉCRASEMENT SILENCIEUX !Le flush repart des valeurspérimées du PC :- status QUALIFIE → VALIDE (rollback logique !)- locked=1 → 0 (rollback logique !)Pas d'exception levée. Les données bulk sont perdues.

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 UPDATE n'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

Anti-patterns — Mélanges interdits dans une même sessionAnti-patterns — Mélanges interdits dans une même sessionSessionDBSessionSessionDBDB✗ Anti-pattern 1 : find() puis Bulk UPDATE puis flush()find() → entité chargée dans PC (version=1, status=VALIDE)HQL bulk UPDATE SET status='QUALIFIE'⚠️ PC toujours status=VALIDE en mémoire !flush() → UPDATE avec status=VALIDE✗ Écrase le bulk silencieusement !✗ Anti-pattern 2 : Bulk UPDATE puis find()HQL bulk UPDATE SET status='QUALIFIE'⚠️ PC non mis à jourfind(id) → retourne cache L1✗ Retourne status=VALIDE (périmé) alors que DB = QUALIFIE✗ Anti-pattern 3 : SQL natif puis flush()SQL natif UPDATE SET locked=1⚠️ PC ignore le SQL natifflush() → UPDATE SET locked=0✗ SQL natif écrasé silencieusement !✗ Anti-pattern 4 : Bulk UPDATE sans VERSIONEDHQL UPDATE sans VERSIONED(version en DB non incrémentée)⚠️ version PC=1, version DB=1mais entité modifiée hors dirty checkingflush() → Hibernate vérifie version✗ OptimisticLockException !
Anti-patterns — Mélanges interdits dans une même sessionAnti-patterns — Mélanges interdits dans une même sessionSessionDBSessionSessionDBDB✗ Anti-pattern 1 : find() puis Bulk UPDATE puis flush()find() → entité chargée dans PC (version=1, status=VALIDE)HQL bulk UPDATE SET status='QUALIFIE'⚠️ PC toujours status=VALIDE en mémoire !flush() → UPDATE avec status=VALIDE✗ Écrase le bulk silencieusement !✗ Anti-pattern 2 : Bulk UPDATE puis find()HQL bulk UPDATE SET status='QUALIFIE'⚠️ PC non mis à jourfind(id) → retourne cache L1✗ Retourne status=VALIDE (périmé) alors que DB = QUALIFIE✗ Anti-pattern 3 : SQL natif puis flush()SQL natif UPDATE SET locked=1⚠️ PC ignore le SQL natifflush() → UPDATE SET locked=0✗ SQL natif écrasé silencieusement !✗ Anti-pattern 4 : Bulk UPDATE sans VERSIONEDHQL UPDATE sans VERSIONED(version en DB non incrémentée)⚠️ version PC=1, version DB=1mais entité modifiée hors dirty checkingflush() → Hibernate vérifie version✗ OptimisticLockException !

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 bulk
Session bulkSession = sessionFactory.openSession();
bulkSession.createQuery("UPDATE Personne SET status=:s WHERE ...").executeUpdate();
bulkSession.flush();
bulkSession.close();

// Session propre pour la suite
Session readSession = sessionFactory.openSession();
Personne Personne = 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évitable
session.createQuery("UPDATE Personne SET status='QUALIFIE' WHERE ...").executeUpdate();
session.flush();
session.clear(); // ← OBLIGATOIRE — vide le PC, force rechargement depuis DB
Personne Personne = 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 HQL
session.createQuery(
    "UPDATE VERSIONED Personne SET status=:s WHERE status=:old")
    .executeUpdate();

Terminé