Les 6 situations où l'annotation @Transactional échoue dans Spring

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

Étiquettes: Spring Framework transactions Java Spring Boot AOP

Publié le 27 juin à 17h30