Un comportement souvent surprenant, modifier un objet sans jamais appeler save(), mais voir la base mise à jour est en réalité le fonctionnement normal de JPA/Hibernate. Voici le déroulement complet.
1. La transaction créée par @Transactional
Quand vous annotez une méthode :
@Transactional
fun refreshCustomer(...) { ... }
Spring démarre automatiquement :
• une transaction de base de données ;
• un contexte de persistance Hibernate (Persistence Context).
Ce contexte joue le rôle d’un cache mémoire où toutes les entités lues ou modifiées sont conservées et suivies.
2. Les entités deviennent « managées »
Quand vous récupérez une entité :
val user = userRepository.findByEmail(email)
L’objet user devient :
• managé (suivi par Hibernate) ;
• stocké dans le contexte de persistance ;
• associé à un snapshot interne représentant son état initial.
Ce snapshot permet à Hibernate de détecter les modifications.
3. Modifier une entité ne déclenche aucune écriture immédiate
Exemple :
user.lastLoginAt = LocalDateTime.now()
Cette modification ne génère aucune requête SQL.
Hibernate note simplement : “Cet objet a changé, je traiterai ça au commit.”
4. Le commit et le Dirty Checking
À la fin de la méthode annotée @Transactional, Spring déclenche le commit.
Avant de valider la transaction, Hibernate exécute le Dirty Checking :
1. Compare l’état actuel de l’objet avec son snapshot.
2. S’il n’y a aucune différence : aucune requête SQL n’est envoyée.
3. Si des modifications sont détectées : Hibernate génère automatiquement une requête UPDATE.
4. La base est synchronisée puis la transaction est validée.
Exemple de SQL généré :
UPDATE users
SET last_login_at = '2025-02-10T10:12:34'
WHERE id = 42;
Aucun appel à save(). Hibernate s’en charge.
Exemple complet
@Transactional
fun refreshUserActivity(email: String) {
// 1. Début de la transaction
val user = userRepository.findByEmail(email)
// 2. Modification de l'entité managée
user.activityUpdatedAt = LocalDateTime.now()
// 3. Fin de méthode -> commit automatique
// Hibernate compare snapshot vs. état actuel
// UPDATE automatique si nécessaire
}
Pas besoin d’appeler accountRepository.save(account).
Pourquoi éviter save() dans une transaction ?
• Réduit le bruit dans le code ;
• Évite des requêtes SQL inutiles ;
• Hibernate met à jour uniquement si l’entité a réellement changé ;
• Plus performant et plus propre.
Le comportement « automatique » observé avec @Transactional provient du couplage entre les transactions Spring et le Dirty Checking de Hibernate.
Ce mécanisme permet :
• de travailler sur des objets Kotlin/Java comme s’ils étaient en mémoire ;
• de laisser Hibernate décider quand envoyer un UPDATE ;
• de réduire le code tout en améliorant les performances.
En résumé : @Transactional + entités managées = mises à jour automatiques et intelligentes.
Une des raisons majeures d’apprécier JPA.