La gestion des transactions constitue un pilier fondamental de la fiabilité des applications orientées données. Ce chapitre explore comment Spring AOP permet d’appliquer des stratégies transactionnelles de manière déclarative, sans modifier la logique métier.
- Pourquoi une gestion transactionnelle est indispensable
Dans une architecture à couches, les méthodes DAO exécutent des opérations unitaires sur la base de données, tandis que les services orchestrent plusieurs opérations pour réaliser une fonctionnalité métier cohérente — par exemple, un virement bancaire impliquant deux mises à jour de solde. Sans transaction, une erreur intermédiaire (comme une clé étrangère invalide ou une exception NullPointerException) laisse la base dans un état incohérent : un compte est débité, mais le crédit n’est jamais appliqué.
- Les fondements ACID
Une transaction valide respecte quatre propriétés essentielles :
- Atomicité : Toutes les opérations réussissent ou échouent ensemble ; aucun état partiel n’est persisté.
- Consistance : La base passe d’un état valide à un autre, conformément aux contraintes métier et relationnelles.
- Isolation : Les exécutions concurrentes ne se perturbent pas mutuellement (problèmes comme les lectures sales ou les lectures fantômes sont évités selon le niveau choisi).
- Persistance : Une fois validée, une transaction résiste aux pannes système.
- Configuration déclarative via annotations
Spring propose une approche non intrusive grâce à l’annotation @Transactional. Voici une implémentation robuste d’un service de transfert :
@Service
public class FundTransferService {
@Autowired
private AccountRepository accountRepo;
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ,
timeout = 30,
rollbackFor = { IllegalArgumentException.class, SQLException.class }
)
public void executeTransfer(String sourceIban, String targetIban, BigDecimal amount) {
var sender = accountRepo.findByIban(sourceIban)
.orElseThrow(() -> new IllegalArgumentException("Compte émetteur introuvable"));
var receiver = accountRepo.findByIban(targetIban)
.orElseThrow(() -> new IllegalArgumentException("Compte destinataire introuvable"));
if (sender.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException("Solde insuffisant");
}
sender.setBalance(sender.getBalance().subtract(amount));
receiver.setBalance(receiver.getBalance().add(amount));
accountRepo.save(sender);
accountRepo.save(receiver);
}
}
- Configuration requise dans le contexte Spring
Pour activer la gestion déclarative, le fichier de configuration doit inclure :
<!-- Gestionnaire de transactions JDBC -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="pooledDataSource"/>
</bean>
<!-- Activation du support des annotations @Transactional -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
- Comportements avancés
Propagation
Le comportement REQUIRED (par défaut) réutilise une transaction existante ou en crée une nouevlle. En revanche, REQUIRES_NEW suspend toute transaction active pour démarrer une transaction indépendante — utile pour journaliser une erreur dans un sous-contexte isolé.
Isolation
Le niveau REPEATABLE_READ garantit que les lectures successives au sein d’une même transaction renvoient les mêmes résultats, ce qui protège contre les lectures non répétables tout en offrant un bon compromis performance/isolation.
Rollback personnalisé
Par défaut, Spring ne déclenche un rollback que sur les exceptions runtime (RuntimeException ou Error). L’attribut rollbackFor permet d’étendre ce comportement à des exceptions vérifiées, comme SQLException.
- Bonnes pratiques critiques
- L’annotation
@Transactionalne fonctionne que sur des méthodes publiques appelées depuis l’extérieur du bean (via le proxy Spring). - Évitez les blocs
try-catchcapturant silencieusement les exceptions — cela empêche le mécanisme de rollback de s’activer. - Assurez-vous que votre moteur de base de données prend en charge les transactions (ex. : InnoDB pour MySQL).
- Privilégiez la granularité au niveau méthode plutôt qu’au niveau classe, sauf si toutes les opérations nécessitent une isolation forte.