🎯 OBJECTIF
Comprendre comment :
persist(), flush() et commit() jouent des rôles distincts et non interchangeablessave(), saveAndFlush(), persist() et merge() se comportent différemment selon le contexte🧠 MODÈLE MENTAL
Hibernate fonctionne comme un tracker de modifications : il maintient un registre interne (le Persistence Context) de toutes les entités qu'il suit. Quand tu modifies une entité MANAGED, Hibernate la détecte automatiquement par comparaison avec son snapshot initial — c'est le dirty checking.
Le SQL n'est pas envoyé à chaque modification, mais regroupé et envoyé au moment du flush (avant le commit ou avant une requête). Ça permet à Hibernate d'optimiser : trois setStatus() successifs ne génèrent qu'un seul UPDATE.
Équation : créer un objet Java ≠ créer une entité JPA. Un objet n'est suivi par Hibernate qu'après persist(). Et le SQL n'est écrit en DB qu'après le flush. Et les données ne sont durables qu'après le commit.
stateDiagram-v2
[*] --> NEW : new Order()
NEW --> MANAGED : persist() / save()
MANAGED --> DETACHED : fin de transaction / evict()
DETACHED --> MANAGED : merge()
MANAGED --> REMOVED : remove()
REMOVED --> [*] : flush + commitmermaidOrder order = new Order();
order.setQty(10);
// Hibernate ne sait pas que cet objet existe — aucun lien DB
em.persist(order); // ✓ NEW → MANAGED, snapshot pris
order.setQty(20); // ✓ Hibernate détecte le changement par dirty checking
// Fin de @Transactional → entité DETACHED
order.setStatus("SHIPPED"); // modification non suivie — aucun UPDATE généréjavapersist() et flush() — rôles distinctssequenceDiagram
participant App as Code applicatif
participant PC as Persistence Context
participant DB as Base de données
App->>PC: persist(order)
Note over PC: order ajouté au registre<br/>état: MANAGED<br/>snapshot pris
Note over DB: ← rien envoyé en DB
App->>PC: order.setStatus("PAID")
Note over PC: dirty checking détecte le changement
App->>PC: flush()
PC->>DB: INSERT INTO orders...
PC->>DB: UPDATE orders SET status=...
Note over DB: SQL exécuté mais PAS commité
Note over DB: rollback encore possiblemermaidem.persist(order); // ✓ NEW → MANAGED, snapshot pris — aucun INSERT en DB
em.flush(); // ✓ SQL généré et envoyé — transaction toujours ouverte
// rollback ici annule tout malgré le flushjava🚨 flush ≠ commit
repository.saveAndFlush(order); // SQL envoyé en DB
throw new RuntimeException(); // transaction rollback
// → aucune donnée persistée définitivementjavaLe flush envoie le SQL. Seul le commit rend les données durables.
order.setStatus(CREATED);
order.setStatus(VALIDATED);
order.setStatus(SHIPPED);
// flush → 1 seul UPDATE avec la valeur finalejavaHibernate optimise en travaillant d'abord en mémoire : il regroupe les opérations redondantes, ordonne les INSERT/UPDATE/DELETE pour respecter les contraintes FK, et réduit les allers-retours réseau.
save(), saveAndFlush(), persist(), merge()| Méthode | Appelle | Flush immédiat | Instance à utiliser |
|---|---|---|---|
persist(entity) |
— | Non | Même instance |
merge(entity) |
— | Non | Instance retournée |
save(entity) |
persist() ou merge() |
Non | Instance retournée |
saveAndFlush(entity) |
save() + flush() |
Oui | Instance retournée |
// ✓ persist() — pour les nouvelles entités
em.persist(order); // Hibernate suit la même instance
// ⚠️ merge() — l'objet passé reste DETACHED
Order managed = em.merge(detachedOrder);
detached.setStatus("SHIPPED"); // ❌ modifie le DETACHED, non suivi
managed.setStatus("SHIPPED"); // ✓ correct
// ✓ save() — abstraction Spring Data
Order saved = repository.save(order);
// Appelle persist() si isNew() → true
// Appelle merge() si isNew() → false
// Toujours utiliser l'instance retournéejava🔑 Conclusion clé
Après merge(), ne jamais continuer à travailler sur l'objet passé en argument — il reste DETACHED. Seule l'instance retournée est suivie par Hibernate.
⚡ TL;DR — chaque concept en une ligne
Mémoire Java vs Persistence Context ✓ Un objet Java ordinaire existe dans le heap JVM. Un objet MANAGED existe aussi dans le Persistence Context d'Hibernate. ⚠ Hibernate ne connaît et ne suit que les entités MANAGED — modifier un objet NEW ne provoque aucun SQL.
persist()
✓ Enregistre une entité NEW dans le Persistence Context → état MANAGED.
⚠ Ne génère aucun SQL immédiatement — l'INSERT sera envoyé lors du flush.
flush()
✓ Synchronise le Persistence Context avec la DB — envoie les INSERT/UPDATE/DELETE en attente.
⚠ Ne valide pas la transaction — un rollback après flush annule tout.
save() (Spring Data)
✓ Abstraction Spring : appelle persist() pour une nouvelle entité, merge() pour une existante.
⚠ Peut masquer les effets de merge() — l'instance retournée par save() est celle à utiliser.
merge()
✓ Copie l'état d'une entité DETACHED dans une nouvelle instance MANAGED.
⚠ L'objet passé en argument reste DETACHED — seule l'instance retournée est suivie.
🎓 À retenir
@Transactional terminé) ne génère aucun UPDATE, sans erreur. C'est le cas classique du DETACHED silencieux.save() sur une entité avec ID déjà défini appelle merge() — Hibernate fait un SELECT avant le UPDATE pour vérifier si l'entité existe. Sur des batch inserts, ça se traduit par N SELECT + N INSERT au lieu de N INSERT. Utiliser persist() directement pour les vrais inserts.flush() manuel avant une contrainte DB permet de la détecter tôt — sans flush(), une violation de contrainte d'unicité ne remonte qu'au commit, souvent dans un contexte où le diagnostic est difficile. saveAndFlush() ou em.flush() explicite permet de la catcher au bon endroit.flush() + clear() entre chunks pour éviter l'OOM en traitement batch