đŹ 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 ```plantuml @startuml title Mix Session ORM + Bulk + SQL natif actor DĂ©veloppeur participant "Session Hibernate\n(PC actif)" as PC database DB DĂ©veloppeur -> PC : 1. find(Personne.class, id)\nâ entitĂ© [version=1,status=VALIDE] note right of PC Tous les accĂšs sont synchronisĂ©s entre eux. Hibernate gĂšre la cohĂ©rence. end note DĂ©veloppeur -> PC : 2. createQuery(\n"UPDATE Personne SET status='QUALIFIE'\nWHERE status='VALIDE'"\n).executeUpdate() note right Le bulk met Ă jour la DB ET les entitĂ©s dĂ©jĂ chargĂ©es dans le PC sont actualisĂ©es. end note DĂ©veloppeur -> PC : 3. session.get(Personne.class, id) note right On a status='QUALIFIE' â end note DĂ©veloppeur -> DB : 4. createNativeQuery(\n"UPDATE Personne SET locked=1\nWHERE id=:id"\n).executeUpdate() note right La session est Ă locked=1 maintenant. end note DĂ©veloppeur -> PC : 5. flush() + commit() note right Tout est cohĂ©rent â end note @enduml ``` --- ## 3. Ce qui se passe RĂELLEMENT ```plantuml @startuml title Ce qui se passe RĂELLEMENT â Le PC devient une source de donnĂ©es pĂ©rimĂ©es actor DĂ©veloppeur participant "Session HibernatePC = cache L1" as PC database DB DĂ©veloppeur -> PC : 1. find(Personne.class, id) PC -> DB : SELECT Personne WHERE id=1 DB --> PC : status=VALIDE, locked=0, version=1 note over PC PC contient : Personne#1 â status=VALIDE,locked=0, version=1 end note DĂ©veloppeur -> PC : 2. HQL Bulk UPDATE createQuery("UPDATE Personne SET\nstatus='QUALIFIE'...").executeUpdate() PC -> DB : UPDATE Personne SET status='QUALIFIE'⥠ExĂ©cution SQL IMMĂDIATE note over PC â ïž Le bulk BYPASSE le PC ! La DB est mise Ă jour directement. Mais le PC garde en mĂ©moire : Personne#1 â status=VALIDE â PĂRIMĂ ! end note DĂ©veloppeur -> PC : 3. find(Personne.class, id) (mĂȘme session, mĂȘme PC) note over 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 ! end note PC --> DĂ©veloppeur : Personne.status = VALIDE â DĂ©veloppeur -> DB : 4. SQL natif UPDATE SET locked=1 WHERE id=1 note over PC â ïž SQL natif bypasse Ă©galement le PC ET le cache L2. PC garde locked=0 en mĂ©moire. end note DĂ©veloppeur -> PC : 5. flush() + commit() PC -> DB : UPDATE Personne SET status=VALIDE, locked=0, version=2\n(dirty checking sur l'entitĂ© en mĂ©moire !) note over DB â ĂCRASEMENT SILENCIEUX ! Le flush repart des valeurs pĂ©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. end note @enduml ``` --- ## 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 ```plantuml @startuml title Anti-patterns â MĂ©langes interdits dans une mĂȘme session == â Anti-pattern 1 : find() puis Bulk UPDATE puis flush() == note over Session, DB : find() â entitĂ© chargĂ©e dans PC (version=1, status=VALIDE) Session -> DB : HQL bulk UPDATE SET status='QUALIFIE' note over Session : â ïž PC toujours status=VALIDE en mĂ©moire ! Session -> DB : flush() â UPDATE avec status=VALIDE note over DB #darkred : â Ăcrase le bulk silencieusement ! == â Anti-pattern 2 : Bulk UPDATE puis find() == Session -> DB : HQL bulk UPDATE SET status='QUALIFIE' note over Session : â ïž PC non mis Ă jour Session -> Session : find(id) â retourne cache L1 note over Session #darkred : â Retourne status=VALIDE (pĂ©rimĂ©) alors que DB = QUALIFIE == â Anti-pattern 3 : SQL natif puis flush() == Session -> DB : SQL natif UPDATE SET locked=1 note over Session : â ïž PC ignore le SQL natif Session -> DB : flush() â UPDATE SET locked=0 note over DB #darkred : â SQL natif Ă©crasĂ© silencieusement ! == â Anti-pattern 4 : Bulk UPDATE sans VERSIONED == Session -> DB : HQL UPDATE sans VERSIONED\n(version en DB non incrĂ©mentĂ©e) note over Session : â ïž version PC=1, version DB=1\nmais entitĂ© modifiĂ©e hors dirty checking Session -> DB : flush() â Hibernate vĂ©rifie version note over Session #darkred : â OptimisticLockException ! @enduml ``` --- ## 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. ```java // â 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 : ```java // â ïž 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 : ```java 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
```java // Force l'incrémentation de @Version lors d'un bulk HQL session.createQuery( "UPDATE VERSIONED Personne SET status=:s WHERE status=:old") .executeUpdate(); ``` --- #
Terminé