🎬 Passer en mode présentation
# Transaction (Jakarta EE ) & cache Retour d'expérience sur des problèmes rencontrés lors de la mise en production. !!!info Je ne vais parler que des analyses auxquelles j'ai participé, il se peut que d'autres problématiques aient été rencontrées. Mais cela n'affecte pas l'explication des concepts que l'on va voir. --- ## Introduction L'annotation **@Transactional** (provenant de Jakarta Transactions) permet de gérer de manière déclarative les transactions dans les applications Java EE/Jakarta EE. Elle contrôle les limites transactionnelles et détermine comment les opérations de base de données sont regroupées et validées.​ L'annotation doit être placée sur les méthodes qui coordonnent plusieurs opérations de base de données. Cela garantit la cohérence des données. Besoin de créer des services métier qui orchestrent les différents services => conception. ## Cycle de vie Lorsqu'une méthode annotée avec @Transactional est invoquée, le conteneur EJB gère automatiquement le cycle de vie de la transaction:​ - Démarrage : Une transaction démarre avant l'exécution de la méthode - Propagation : Le contexte transactionnel est propagé aux autres services appelés - Validation (commit) : Si la méthode se termine normalement, la transaction est validée - Annulation (rollback) : En cas d'exception runtime, la transaction est annulée --- ## Mode de propagation L'annotation supporte différents modes de propagation qui déterminent comment les transactions se comportent lors d'appels imbriqués:​ - **REQUIRED** (par défaut) : Rejoint une transaction existante ou en crée une nouvelle si aucune n'existe. C'est le comportement standard des EJB.​ - **REQUIRES_NEW** : Suspend la transaction courante et crée une nouvelle transaction indépendante. Les deux transactions sont isolées : le succès ou l'échec de l'une n'affecte pas l'autre.​ - **MANDATORY** : Exige qu'une transaction existe déjà , sinon une exception est levée.​ - **SUPPORTS** : Exécute avec une transaction si elle existe, sinon s'exécute sans transaction.​ - **NOT_SUPPORTED** : Suspend toute transaction existante et s'exécute en dehors de tout contexte transactionnel.​ - **NEVER** : Lève une exception si une transaction est active.​ --- ## Limites - Appels locaux : L'annotation ne fonctionne que pour les appels via proxy. Les appels de méthodes internes à la même classe ne déclenchent pas le mécanisme transactionnel.​ - Exceptions checked : Par défaut, seules les exceptions runtime provoquent un rollback. Les exceptions checked nécessitent une configuration explicite.​ - Performance : Chaque transaction implique un coût (verrouillage, synchronisation). Évitez les transactions trop longues.​ --- ## Etude de cas 1 Problème rencontré : Batch création préadmission Le batch s'exécute en 3 temps : création, suppression et modification de rdv. A Castres ce batch était systématiquement en erreur => avec l'erreur suivante : !!!danger OptimisticLockException sur patient lors du chargerPatientParIpp ### Explications - Pourquoi cette erreur sur une lecture ? - Avant la requête, Hibernate applique toutes les modifications non encore propagées vers la base de données (flush), pour répondre à la contrainte de cohérence transactionnelle même sur une lecture. Cela permet d'avoir la donnée cohérente dans la transaction. - Pourquoi cette erreur alors qu'on est dans une transaction ? - Le premier problème est le fait que la transaction est globale aux 3 actions (même si chaque dossier est dans une transaction REQUIRES_NEW) Cela va à l'encontre de ce qui est dit plus haut 'évitez les transactions trop longues' elle doit être au plus près du besoin. il faut une transaction par action pour repartir d'une BDD à jour entre chaque action. - deuxième problème est le fait que la modification est générée par le batch lui-même. Au fil des itérations on a ajouté une fonctionnalité sur l'INS qui s'effectue dans une transaction REQUIRES_NEW qui modifie le patient. Lorsqu'on revient dans la transaction parente le patient n'est plus cohérent. --- ### Visualisation du problème ```puml @startuml skinparam backgroundColor transparent skinparam shadowing false skinparam borderColor #4f5f72 title Transaction REQUIRED vs REQUIRES_NEW - Problème OptimisticLockException state "ÉTAPE 1 : Transaction T1 démarre" as etape1 #434c5e note left of etape1 #3b4252 Service 1 : @Transactional(REQUIRED) Transaction T1 ouverte end note state "ÉTAPE 2 : chargement des données" as etape2 #434c5e state "ÉTAT Transaction 1 : Chargement initial dans T1" as transac1 #434c5e { state "Persistence Context T1" as pc1 #darkgoldenrod { state "A(id=1, version=1)\nManaged" as a1_v1 #4c566a } state "Base de données" as db1 #2e3440 { state "A(id=1, version=1)" as db_a1_v1 #4c566a } } note left of etape2 #3b4252 SELECT A WHERE id=1 A chargé dans le cache de T1 end note state "ÉTAPE 3 : Appel Service 2 (REQUIRES_NEW)" as etape3 #d08670ac note left of etape3 #3b4252 Service 2 : @Transactional(REQUIRES_NEW) T1 suspendue, T2 créée Modifie A et commit T2 end note state "ÉTAT Transaction 2 : Après commit de T2" as transac2 #434c5e { state "Persistence Context T1" as pc2 #darkgoldenrod { state "A(id=1, version=1)\nManaged\n⚠️ Ancienne version" as a1_v1b #d08670ac } state "Base de données" as db2 #2e3440 { state "A(id=1, version=2)\n✓ Modifié par T2" as db_a1_v2 #5e81ac } } note right of transac2 #3b4252 Transaction T2 committée BD : version=2 Mais T1 a toujours version=1 en mémoire! end note state "ÉTAPE 4 : Rechargement dans T1" as etape4 #434c5e note left of etape4 #3b4252 SELECT A (nouveau chargement) Hibernate doit faire un FLUSH avant le SELECT end note state "ÉTAT Transaction 3 : Tentative de FLUSH" as transac3 #434c5e { state "Persistence Context T1" as pc3 #darkgoldenrod { state "A(id=1, version=1)\nTente UPDATE" as a1_flush #bf616a } state "Base de données" as db3 #2e3440 { state "A(id=1, version=2)\nREJET!" as db_reject #bf616a } } state "ÉTAPE 5 : OptimisticLockException" as etape5 #bf616a note left of etape5 #3b4252 UPDATE A SET ... WHERE id=1 AND version=1 ❌ Aucune ligne mise à jour (version attendue = 2) OptimisticLockException levée end note etape1 -down-> etape2 etape2 -right-> transac1 etape2 -down-> etape3 etape3 -right-> transac2 etape3 -down-> etape4 etape4 -right-> transac3 etape4 --> etape5 legend right |= Couleur |= Signification | | <#4c566a> | Entité version initiale | | <#5e81ac> | Modifié en BD (nouvelle version) | | <#d08670ac> | Version obsolète (conflit) | | <#bf616a> | Erreur / Rejet | | <#darkgoldenrod> | Persistence Context (mémoire) | | <#2e3440> | Base de données | endlegend note bottom #3b4252 **Problème :** T2 (REQUIRES_NEW) modifie la BD sans mettre à jour le Persistence Context de T1. Quand T1 tente un flush, les versions divergent. end note @enduml ``` --- ## Etude de cas 2 : cache Dans un contexte particulier, on chargeait une donnée avec une requête HQL qui permettait de renseigner les dépendances à charger. Pourtant on rencontrait lors de l'accés aux données dépendantes des erreurs LazyInitializationException. ### Analyse du problème On s'est rendu compte qu'une première requête HQL chargeait le même type de données sans aucune dépendance et que c'est cette exécution qui engendrait le problème. ### Explications Cache de 1ᵉʳ niveau (Persistence Context) : - Chaque transaction possède son propre cache qui gère les entités managed chargées au cours de la transaction. - Le cache ne peut contenir qu'une seule instance d'une entité pour une transaction. Comportement prévu par hibernate : - la première requête HQL charge les données sans aucune dépendance et les données sont placées dans le cache. - la seconde requête HQL est bien exécuté avec les dépendances. - cependant lors de la mise en cache des données si celles-ci existent déjà , elles ne sont pas remplacées et les relation sont ignorés - c'est la donnée du cache qui est utilisé par hibernate(sans les relations). --- ```puml @startuml skinparam shadowing false skinparam backgroundColor transparent title Évolution du Cache Hibernate - Étude de cas HQL avec JOIN FETCH state "ÉTAPE 1 : État Initial" as etape1 #434c5e note left of etape1 #3b4252 Cache vide Transaction démarre end note state "ÉTAPE 2 : Requête 1" as etape2 #434c5e note left of etape2 #3b4252 HQL: FROM A WHERE id IN (1,2,3) end note state "ÉTAPE 3 : Cache après Requête 1" as cache1 #darkgoldenrod { state "A(id=1)\nB = proxy lazy ❌" as a1 #4c566a state "A(id=2)\nB = proxy lazy ❌" as a2 #4c566a } state "ÉTAPE 4 : Requête 2" as etape4 #434c5e note left of etape4 #3b4252 HQL: FROM A a LEFT JOIN FETCH a.bList WHERE a.id IN (2,3,4) end note state "ÉTAPE 5 : Cache après Requête 2" as cache2 #darkgoldenrod { state "Entités déjà présentes" as old #2e3440 { state "A(1) - lazy ❌" as a1b #5e81ac state "A(2) - lazy ❌\n⚠️ JOIN ignoré" as a3b #bf616a } state "Nouvelles entités" as new #2e3440 { state "A(3) - [B1,B2] ✓" as a4b #5e81ac state "A(4) - [B3] ✓" as a5b #5e81ac } } state "ÉTAPE 6 : Résultat - Accessibilité" as etape6 #darkgoldenrod { state "A(1).getBList() ❌" as r1 #d08670ac state "A(2).getBList() ❌" as r2 #bf616a state "A(3).getBList() ✓" as r3 #5e81ac state "A(4).getBList() ✓" as r4 #5e81ac } etape1 -down-> etape2 etape2 -right-> cache1 etape2 -down-> etape4 etape4 -right-> cache2 etape4 -down-> etape6 legend right |= Couleur |= Signification | | <#d08670ac> | Sans relations (proxy lazy) | | <#5e81ac> | Avec relations chargées | | <#bf616a> | Problématique (intersection) | endlegend note right of cache2 #3b4252 **Règle du cache Hibernate:** Une entité = Une instance unique par ID Le premier chargement définit l'état Les chargements suivants réutilisent l'instance end note @enduml ``` --- #
Terminé