Gestion des transactions
La gestion des transactions est fondamentale dans le développement d'applications. Spring propose des mécanismes robustes, principalement sous deux formes : la programmation manuelle et la déclaration via annotations.
La gestion programmatique implique un contrôle explicite des commits et rollbacks dans le code, ce qui augmente le couplage. Exemple :
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
effectuerOperations();
transactionManager.commit(status);
} catch (Exception ex) {
transactionManager.rollback(status);
throw new ErreurMetier("Échec de la transaction");
}
La gestion déclarative repose sur AOP (programmation orientée aspect), découplant la logique métier de la gestion transactionnelle. L'annotation @Transactional est couramment utilisée pour cette approche.
@Transactional
@PostMapping("/creer-entite")
public Reponse creerEntite(EntiteDto dto) {
Entite entite = convertirEnEntite(dto);
return repository.save(entite);
}
Présentation de l'annotation @Transactional
Où appliquer l'annotation ?
L'annotation @Transactional peut être placée sur des interfaces, des classes ou des méthodes.
- Sur une classe : applique les mêmes paramètres transactionnels à toutes les méthodes pulbiques.
- Sur une méthode : les paramètres de la méthode prévalent sur ceux de la classe.
- Sur une interface : déconseillé, car cela peut provoquer un échec lorsque Spring AOP utilise CGLib.
@Service
@Transactional
public class ServiceCommande {
@Autowired
private CommandeRepository commandeRepo;
@Transactional(rollbackFor = ErreurTechnique.class)
public Commande creerCommande(CommandeDto dto) {
Commande commande = new Commande(dto.getReference());
commande.setStatut("EN_COURS");
return commandeRepo.save(commande);
}
}
Propriétés principales
Propagation
Définit le comportement de propagation par défaut à Propagation.REQUIRED :
REQUIRED: rejoint la transaction existante ou en crée une nouvelle.SUPPORTS: exécution avec ou sans transaction, selon le contexte.MANDATORY: nécessite une transaction existante, sinon exception.REQUIRES_NEW: crée une nouvelle transaction, suspend la transaction courante.NOT_SUPPORTED: exécution sans transaction, suspend toute transactino en cours.NEVER: exécution sans transaction, exception si une transaction est active.NESTED: similaire àREQUIRED.
Isolation
Niveau d'isolation des transactions, par défaut Isolation.DEFAULT (utilise le niveau de la base de données).
Timeout
Durée maximale avant rollback automatique, par défaut -1 (pas de limite).
ReadOnly
Indique si la transaction est en lecture seule (par défaut false).
RollbackFor et NoRollbackFor
rollbackFor spécifie les types d'exceptions déclenchant un rollback. noRollbackFor désactive le rollback pour certaines exceptions.
Scénarios d'échec de @Transactional
1. Méthode non publique
L'annotation est ignorée sur les méthodes non publiques (protected, private). Cela est dû à l'interception par Spring AOP.
protected TransactionAttribute determinerAttributTransactionnel(Method methode, Class> classeCible) {
if (autoriserSeulementMethodesPubliques() && !Modifier.isPublic(methode.getModifiers())) {
return null;
}
// Suite du traitement...
}
2. Propagation mal configurée
Les modes SUPPORTS, NOT_SUPPORTED et NEVER peuvent empêcher le rollback si une transaction est attendue.
3. RollbackFor incorrect
Spring ne rollbacke que les exceptions non contrôlées (héritant de RuntimeException ou Error). Pour les exceptions personnalisées, il faut spécifier rollbackFor.
@Transactional(rollbackFor = ExceptionMetier.class)
public void traiterDonnees() throws ExceptionMetier {
// Logique métier...
if (conditionEchec) {
throw new ExceptionMetier("Données invalides");
}
}
Spring vérifie la hiérarchie des exceptions :
private int calculerProfondeur(Class> classeException, int profondeur) {
if (classeException.getName().contains(this.nomException)) {
return profondeur;
}
if (classeException == Throwable.class) {
return -1;
}
return calculerProfondeur(classeException.getSuperclass(), profondeur + 1);
}
4. Appels internes dans la même clase
Si une méthode A appelle une méthode B dans la même classe, et que B est annotée @Transactional mais pas A, la transaction de B ne sera pas active. Cela est dû au proxy AOP.
public class ServiceStock {
public void mettreAJourStock(ProduitDto produit) {
// Appel interne sans transaction active
enregistrerMouvement(produit);
}
@Transactional
public void enregistrerMouvement(ProduitDto produit) {
// Cette transaction ne sera pas gérée si appelée depuis mettreAJourStock
mouvementRepo.save(new Mouvement(produit));
}
}
5. Exceptions capturées sans relance
Si une exception est catchée et non relancée, la transaction ne peut pas rollback.
@Transactional
public void executerProcessus() {
try {
etape1();
etape2();
} catch (Exception ex) {
logger.error("Erreur capturée", ex);
// L'exception n'est pas relancée, la transaction ne rollback pas
}
}
Spring déclenche alors une UnexpectedRollbackException car la transaction est marquée pour rollback mais le commit est tenté.
6. Moteur de base de données sans support transactionnel
Par exemple, MySQL avec InnoDB supporte les transactions, mais MyISAM ne le fait pas. Le moteur doit être vérifié.