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.
2. Vue Hibernate
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)
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 ?
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_NEWviathis.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_SUPPORTEDsur la méthode de boucle : élimine tout PC résiduel, tout lock, tout snapshot périméStatelessSessionuniquement 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