🎬 Passer en mode présentation
# Hibernate / transaction avec EJB dans JBoss --- ## 1. Le scénario du développeur On charge une liste d'entités dans un EJB `@REQUIRED` (valeur par défaut si pas d'annotation), puis on boucle dessus en appelant un EJB `@REQUIRES_NEW` pour chaque item afin d'isoler les traitements unitaires et permettre un commit indépendant par item. ```plantuml @startuml title Exemple problématique actor Appelant participant "EJB A\n@REQUIRED TX1" as EA participant "PC1 lié à TX1" as PC1 participant "EJB B\n@REQUIRES_NEW TX2" as EB database DB Appelant -> EA : process() EA -> DB : SELECT * FROM personnes\n(1 requête — entités dans PC1) loop Pour chaque entité EA -> EB : traiterPersonne(entity) note right TX2 isolée, PC1 propre, entity déjà en cache, end note EB -> DB : UPDATE personne ... EB -> EB : commit() TX2 ✅ end EA -> EA : commit() TX1 ✅ note right TX1 commit() avec son cache end note @enduml ``` --- ## 2. Vue Hibernate ```plantuml @startuml title Ce qui se passe RÉELLEMENT actor Appelant participant "EJB A\n@REQUIRED TX1" as EA participant "PC1\nversion=1 en mémoire" as PC1 participant "EJB B\n@REQUIRES_NEW TX2" as EB participant "PC2 vide" as PC2 database DB Appelant -> EA : process() EA -> DB : SELECT * FROM personnes\n(entités chargées dans PC1\nversion=1) loop Pour chaque entité passée à EJB B EA -> EB : traiterPersonne(entity)\n⚠️ entity appartient à PC1 note over PC2 PC2 est VIDE. L'entité reçue est DÉTACHÉE du point de vue de PC2. end note EB -> PC2 : merge(entity) PC2 -> DB : ⚠️ SELECT personne WHERE id=?\n(N+1 requêtes !) PC2 -> DB : UPDATE personne SET version=2 EB -> EB : commit() TX2\n→ DB : version=2 note over PC1 PC1 garde version=1 en mémoire ⚠️ end note end EA -> PC1 : flush() TX1 PC1 -> DB : UPDATE avec version=1\nmais DB = version=2 DB --> EA : ❌ OptimisticLockException ! @enduml ``` --- ## 3. Les problématiques soulevées ####
Problème 1 — N+1 requêtes
Chaque `REQUIRES_NEW` crée un nouveau contexte de persitence (PC2) vide. L'entité passée en paramètre appartenant à PC1 est **détachée** du point de vue de PC2. Le `merge()` déclenche un `SELECT` pour la rattacher. Résultat : **N SELECT** en plus des requêtes métier, multiplié par le nombre d'associations lazy accédées. ####
Problème 2 — OptimisticLockException
PC1 garde en mémoire les entités avec `version=1`. Chaque TX2 commite et passe la version à `2` en base. Au flush final de TX1 : Hibernate tente d'écrire avec `version=1` sur des lignes qui ont déjà `version=2` → `StaleObjectStateException`. C'est **systématique** dès qu'une entité chargée dans PC1 est modifiée dans un `REQUIRES_NEW`. --- ####
Problème 3 — Données périmées (snapshot isolation)
Même si TX1 ne modifie pas les entités, son snapshot date du début de TX1. Les données relues dans TX1 **après** la boucle sont potentiellement périmées, les modifications de TX2 n'étant pas visibles dans PC1. ####
Problème 4 — Deadlock potentiel
TX1 suspendue maintient des **locks de lecture** sur les lignes chargées. TX2 tente un `UPDATE` sur ces mêmes lignes. Selon le niveau d'isolation et le moteur DB → **deadlock possible** entre TX1 suspendue et TX2 active. ####
Problème 5 — Épuisement du pool de connexions
La connexion de TX1 reste **ouverte et suspendue** pendant toute la durée de chaque TX2. Sur N itérations longues ou parallèles → pression sur le pool de connexions JBoss. --- ####
Problème 6 — Self-invocation (piège EJB spécifique)
```plantuml @startuml title Piège : this.traiterPersonne() bypasse le proxy JBoss participant "EJB A process()" as EA participant "Proxy JBoss\n(intercepteur CMT)" as PROXY participant "EJB A processOne()\n@REQUIRES_NEW" as EA2 EA -> EA2 : this.processOne(entity) note over PROXY ⚠️ Appel direct via "this" Le proxy JBoss est bypassé @REQUIRES_NEW est IGNORÉ (pas de nouvelle transaction) → s'exécute dans TX1 end note @enduml ``` L'annotation `@REQUIRES_NEW` n'est honorée **que si l'appel passe par le proxy du conteneur** — via injection `@EJB` ou depuis un EJB distinct. --- ## 4. Les solutions — Avantages et inconvénients ### Solution A — Bulk UPDATE (JPQL/HQL) ```java em.createQuery( "UPDATE personne o SET o.status = :new " + "WHERE o.status = :old AND o.createdAt < :limit") .executeUpdate(); ``` | ✅ Avantages | ❌ Inconvénients | | :-- | :-- | | 1 seul `UPDATE` SQL, ultra-performant | Pas de logique Java par item | | Aucune entité chargée en mémoire | Pas de gestion d'erreur unitaire | | Zéro N+1, zéro OptimisticLock | Pas de cascade sur les relations | | Scalable sur des millions de lignes | Bypass du cache L2 et des events Hibernate | **Quand l'utiliser :** modification uniforme de colonnes scalaires sur un grand ensemble, sans logique métier individuelle. --- ### Solution B — `StatelessSession` ```java ScrollableResults scroll = statelessSession.createQuery( "FROM personne o WHERE o.status = :s") .scroll(ScrollMode.FORWARD_ONLY); while (scroll.next()) { personne personne = (personne) scroll.get(0); personne.setStatus(computeNewStatus(personne)); // logique Java par item statelessSession.update(personne); } ``` | ✅ Avantages | ❌ Inconvénients | | :-- | :-- | | Logique Java individuelle possible | Pas de cascade (insert/update/delete) | | Pas de cache L1 → pas de fuite mémoire | Collections lazy inaccessibles | | Performant sur gros volumes | Pas d'events, interceptors, L2 cache | | Pas d'OptimisticLock par défaut | Gestion manuelle des relations | **Quand l'utiliser :** logique Java individuelle par item sur des **champs scalaires uniquement**, sans modification de relations ou collections. --- ### Solution C — Projection DTO + `REQUIRES_NEW` par ID ```java // EJB A — NOT_SUPPORTED List
dtos = em.createQuery( "SELECT NEW com.example.personnesummaryDTO(o.id, o.reference, o.totalAmount) " + "FROM personne o WHERE o.status = :s", personnesummaryDTO.class) .getResultList(); for (personnesummaryDTO dto : dtos) { ejbB.processOne(dto.getId()); // ID scalaire uniquement } // EJB B — REQUIRES_NEW public void processOne(Long personneId) { personne personne = em.find(personne.class, personneId); // PC propre, entité fraîche // + JOIN FETCH si collections nécessaires } ``` | ✅ Avantages | ❌ Inconvénients | | :-- | :-- | | PC2 propre à chaque itération | N+1 SELECT inévitable (1 par item) | | Pas d'OptimisticLock (entité fraîche dans PC2) | Pool de connexions sollicité | | Commit/rollback unitaire réel | Moins performant que bulk | | Logique métier complexe possible | | | Cascade et collections fonctionnelles | | | Compatible avec contrainte "pas de rollback global" | | **Quand l'utiliser :** logique métier complexe par item, avec relations et cascades, contrainte de commit unitaire indépendant. --- ### Solution D — `flush()` + `clear()` périodiques (rollback global accepté) ```java // EJB A — REQUIRED, 1 seul PC int i = 0; for (personne personne : personnes) { personne.setStatus(Status.PROCESSED); if (++i % 50 == 0) { em.flush(); // écriture en DB em.clear(); // libération mémoire PC } } em.flush(); ``` | ✅ Avantages | ❌ Inconvénients | | :-- | :-- | | 1 seule transaction, cohérence totale | Rollback global si 1 erreur | | Pas de N+1 (entités déjà dans PC) | Pas de commit unitaire | | Cascade et dirty checking actifs | Mémoire croissante entre les `clear()` | | Simple à implémenter | Moins adapté aux erreurs partielles | **Quand l'utiliser :** traitement en lot avec **rollback global souhaité** en cas d'erreur. --- ## 5. Arbre de décision — Quelle solution choisir ? ```plantuml @startuml title Arbre de décision — Bonne pratique Hibernate/EJB/JBoss start :Traitement en boucle\nsur N entités; if (Modification uniforme\nsans logique Java\npar item ?) then (OUI) :✅ Bulk UPDATE JPQL/HQL; note right Préconisation n°1 1 seul UPDATE SQL Scalable à l'infini end note stop else (NON) if (Rollback global\naccepté si erreur ?) then (OUI) :✅ REQUIRED + flush/clear\npériodiques; note right Préconisation n°2 si cohérence totale requise end note stop else (NON — commit unitaire requis) if (Modifications\nuniquement sur\nchamps scalaires ?) then (OUI) :✅ StatelessSession; note right Préconisation n°3 Pas de cascade, pas de collections end note stop else (NON — relations/collections impliquées) :✅ Projection DTO\n+ REQUIRES_NEW par ID; note right Préconisation n°4 N+1 assumé et maîtrisé Seule option viable pour logique complexe + commit unitaire end note stop endif endif endif @enduml ``` --- ## 6.Bonnes pratiques - **Ne jamais passer une entité attachée** (PC1) en paramètre à un `REQUIRES_NEW` → merge parasite + N+1 + OptimisticLock - **Ne jamais appeler `REQUIRES_NEW` via `this.`** dans le même EJB → proxy bypassé, annotation ignorée - **Préférer les IDs scalaires ou DTOs** comme pivot entre la boucle et les traitements unitaires - **`NOT_SUPPORTED` sur la méthode de boucle** : élimine tout PC résiduel, tout lock, tout snapshot périmé - **`StatelessSession` uniquement pour les champs scalaires** sans cascade, sans collections - **Bulk UPDATE en priorité** dès que la logique le permet — c'est systématiquement la solution la plus performante et la plus simple #
Terminé