🎬 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.

Exemple problématiqueExemple problématiqueAppelantEJB APC1 li. . TX1EJB BDBAppelantAppelantEJB A@REQUIRED TX1EJB A@REQUIRED TX1PC1 lié à TX1PC1 lié à TX1EJB B@REQUIRES_NEW TX2EJB B@REQUIRES_NEW TX2DBDBprocess()SELECT * FROM personnes(1 requête — entités dans PC1)loop[Pour chaque entité]traiterPersonne(entity)TX2 isolée, PC1 propre,entity déjà en cache,UPDATE personne ...commit() TX2 ✅commit() TX1 ✅TX1 commit() avec son cache
Exemple problématiqueExemple problématiqueAppelantEJB APC1 li. . TX1EJB BDBAppelantAppelantEJB A@REQUIRED TX1EJB A@REQUIRED TX1PC1 lié à TX1PC1 lié à TX1EJB B@REQUIRES_NEW TX2EJB B@REQUIRES_NEW TX2DBDBprocess()SELECT * FROM personnes(1 requête — entités dans PC1)loop[Pour chaque entité]traiterPersonne(entity)TX2 isolée, PC1 propre,entity déjà en cache,UPDATE personne ...commit() TX2 ✅commit() TX1 ✅TX1 commit() avec son cache

2. Vue Hibernate

Ce qui se passe RÉELLEMENTCe qui se passe RÉELLEMENTAppelantEJB APC1EJB BPC2 videDBAppelantAppelantEJB A@REQUIRED TX1EJB A@REQUIRED TX1PC1version=1 en mémoirePC1version=1 en mémoireEJB B@REQUIRES_NEW TX2EJB B@REQUIRES_NEW TX2PC2 videPC2 videDBDBprocess()SELECT * FROM personnes(entités chargées dans PC1version=1)loop[Pour chaque entité passée à EJB B]traiterPersonne(entity)⚠️ entity appartient à PC1PC2 est VIDE.L'entité reçue est DÉTACHÉEdu point de vue de PC2.merge(entity)⚠️ SELECT personne WHERE id=?(N+1 requêtes !)UPDATE personne SET version=2commit() TX2→ DB : version=2PC1 garde version=1en mémoire ⚠️flush() TX1UPDATE avec version=1mais DB = version=2❌ OptimisticLockException !
Ce qui se passe RÉELLEMENTCe qui se passe RÉELLEMENTAppelantEJB APC1EJB BPC2 videDBAppelantAppelantEJB A@REQUIRED TX1EJB A@REQUIRED TX1PC1version=1 en mémoirePC1version=1 en mémoireEJB B@REQUIRES_NEW TX2EJB B@REQUIRES_NEW TX2PC2 videPC2 videDBDBprocess()SELECT * FROM personnes(entités chargées dans PC1version=1)loop[Pour chaque entité passée à EJB B]traiterPersonne(entity)⚠️ entity appartient à PC1PC2 est VIDE.L'entité reçue est DÉTACHÉEdu point de vue de PC2.merge(entity)⚠️ SELECT personne WHERE id=?(N+1 requêtes !)UPDATE personne SET version=2commit() TX2→ DB : version=2PC1 garde version=1en mémoire ⚠️flush() TX1UPDATE avec version=1mais DB = version=2❌ OptimisticLockException !

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=2StaleObjectStateException. 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)

Piège : this.traiterPersonne() bypasse le proxy JBossPiège : this.traiterPersonne() bypasse le proxy JBossEJB A process..Proxy JBossEJB A processOne..EJB A process()EJB A process()Proxy JBoss(intercepteur CMT)Proxy JBoss(intercepteur CMT)EJB A processOne()@REQUIRES_NEWEJB A processOne()@REQUIRES_NEWthis.processOne(entity)⚠️ Appel direct via "this"Le proxy JBoss est bypassé@REQUIRES_NEW est IGNORÉ (pas de nouvelle transaction)→ s'exécute dans TX1
Piège : this.traiterPersonne() bypasse le proxy JBossPiège : this.traiterPersonne() bypasse le proxy JBossEJB A process..Proxy JBossEJB A processOne..EJB A process()EJB A process()Proxy JBoss(intercepteur CMT)Proxy JBoss(intercepteur CMT)EJB A processOne()@REQUIRES_NEWEJB A processOne()@REQUIRES_NEWthis.processOne(entity)⚠️ Appel direct via "this"Le proxy JBoss est bypassé@REQUIRES_NEW est IGNORÉ (pas de nouvelle transaction)→ s'exécute dans TX1

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)

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

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

// EJB A — NOT_SUPPORTED
List<personnesummaryDTO> 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é)

// 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 ?

Arbre de décision — Bonne pratique Hibernate/EJB/JBossArbre de décision — Bonne pratique Hibernate/EJB/JBossTraitement en bouclesur N entitésModification uniformesans logique Javapar item ?OUINONPréconisation n°11 seul UPDATE SQLScalable à l'infini✅ Bulk UPDATE JPQL/HQLRollback globalaccepté si erreur ?OUINON — commit unitaire requisPréconisation n°2si cohérence totale requise✅ REQUIRED + flush/clearpériodiquesModificationsuniquement surchamps scalaires ?OUINON — relations/collections impliquéesPréconisation n°3Pas de cascade,pas de collections✅ StatelessSessionPréconisation n°4N+1 assumé et maîtriséSeule option viablepour logique complexe+ commit unitaire✅ Projection DTO+ REQUIRES_NEW par ID
Arbre de décision — Bonne pratique Hibernate/EJB/JBossArbre de décision — Bonne pratique Hibernate/EJB/JBossTraitement en bouclesur N entitésModification uniformesans logique Javapar item ?OUINONPréconisation n°11 seul UPDATE SQLScalable à l'infini✅ Bulk UPDATE JPQL/HQLRollback globalaccepté si erreur ?OUINON — commit unitaire requisPréconisation n°2si cohérence totale requise✅ REQUIRED + flush/clearpériodiquesModificationsuniquement surchamps scalaires ?OUINON — relations/collections impliquéesPréconisation n°3Pas de cascade,pas de collections✅ StatelessSessionPréconisation n°4N+1 assumé et maîtriséSeule option viablepour logique complexe+ commit unitaire✅ Projection DTO+ REQUIRES_NEW par ID

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é