1. Le rôle du flush() explicite
Le flush() explicite sert avant tout de point de contrôle de synchronisation. Il permet de ne pas attendre qu'Hibernate choisisse le moment lui-même. Pour une opération métier ordinaire, ce n'est généralement pas nécessaire, mais en diagnostic, sans un tel point de contrôle, on se noie vite : difficile de savoir quel bloc de code a généré le SQL, où la contrainte de base s'est déclenchée, et si on consulte la base ou si on regarde toujours la même copie managed.
Le flush() explicite n'est pas un bouton «rends tout plus fiable» et ne remplace pas le commit. C'est un outil d'observation et de diagnostic : tu dis explicitement à Hibernate «synchronise le contexte ici, pour que je voie le SQL, que j'attrape l'erreur de base au plus près du problème, ou que je relise honnêtement l'état depuis la base».
flush() : mémoire, base et commit
Ici, une simple formule suffit : flush() envoie les INSERT/UPDATE/DELETE accumulés en base dans le cadre de la transaction courante, et le commit termine la transaction et valide les changements définitivement. Donc après flush(), le SQL est déjà visible, une erreur de contrainte peut déjà être attrapée, mais un rollback peut encore tout annuler.
Cette distinction suffit à ne pas confondre le flush() de diagnostic avec la finalisation de la transaction. La combinaison flush() + clear() aide à lire la base honnêtement, et le flush() explicite aide à attraper les erreurs plus tôt.
2. La combinaison flush() + clear() pour une lecture honnête de la base
Si tu veux vérifier que «en base, c'est vraiment devenu ainsi», il y a un piège subtil : à l'intérieur du persistence context, Hibernate essaie d'être utile et économe. Si tu fais find(Product.class, 1L) deux fois dans la même transaction, la deuxième fois le SQL peut ne pas partir du tout — tu récupères le même objet managed depuis le first-level cache. Et c'est normal : l'ORM protège l'identity map et garantit «une ligne — un objet».
C'est pour ça que si tu veux consciemment voir la différence entre «j'ai le nouveau nom en mémoire» et «j'ai le nouveau nom en base», il faut souvent une combinaison : d'abord flush(), puis clear(), puis une nouvelle lecture. Le sens est simple : flush() pousse les changements en base, et clear() éjecte la copie managed pour que le prochain find() aille vraiment en base.
Mini-scénario sur Product à l'intérieur d'une transaction :
import jakarta.persistence.EntityManager;
// 1) On lit l'entité : elle devient managed dans le persistence context courant
Product p = entityManager.find(Product.class, 1L);
// 2) On change le champ : pour l'instant c'est juste un changement en mémoire (marqué comme dirty)
p.setName("Phone Pro");
// 3) On pousse explicitement les changements accumulés en SQL tout de suite
entityManager.flush(); // un UPDATE apparaît dans le SQL log
// 4) On éjecte les objets managed du persistence context,
// pour que la prochaine lecture ne renvoie pas la même référence Java depuis le cache de niveau 1
entityManager.clear();
// 5) Maintenant ce sera une re-lecture depuis la base (SELECT), pas un retour depuis le first-level cache
Product reread = entityManager.find(Product.class, 1L);
Et un détail important qu'on rate facilement : la variable p ne disparaît nulle part après clear(). C'est toujours le même objet Java, mais pour le persistence context courant, il est maintenant detached. Si après clear() tu continues d'appeler p.setName(...), Hibernate ne le verra pas : le dirty checking ne fonctionne que sur les entités managed, jusqu'à ce que tu relises l'objet ou que tu le remettes explicitement dans le contexte.
Il y a ici un effet psychologique important : après clear(), tu commences à ne faire confiance qu'à ce que tu peux à nouveau lire depuis la base. Et c'est une très bonne habitude quand tu débogues «pourquoi les requêtes se comportent bizarrement» ou «pourquoi un UPDATE inattendu est apparu dans le log».
Dans notre Commerce Persistence Lab, cette combinaison est pratique à utiliser dans de petits scénarios de laboratoire — par exemple dans un composant qui démarre avec l'application en profil dev et fait une expérience bien définie pendant qu'on regarde le SQL trace. Mais même sans runner spécial, c'est une excellente technique pour l'analyse dans le débogueur.
3. clear() sans flush() : «effacer» les changements
Si la combinaison flush() + clear() aide à relire honnêtement l'état depuis la base, alors la combinaison inverse — clear() sans flush() — c'est une façon de faire comme si les changements n'avaient jamais existé. C'est parfois utile (par exemple si tu veux délibérément jeter tous les changements managed accumulés), mais le plus souvent c'est un piège : quelqu'un fait clear() «pour relire», puis s'étonne que la base «n'ait pas été mise à jour».
Imagine que tu as modifié une entité managed, mais qu'il n'y a pas encore eu de flush ni automatique ni explicite. Le changement vit uniquement dans le persistence context. Si tu fais clear(), tu éjectes ce contexte. Hibernate n'est plus obligé de se souvenir de ton changement, parce que tu lui as toi-même demandé de «tout oublier». Et la base, naturellement, n'a rien su, parce que le SQL n'a jamais été envoyé.
Voici un court exemple à garder en tête comme anti-incantation :
import jakarta.persistence.EntityManager;
// 1) Entité managed
Product p = entityManager.find(Product.class, 1L);
// 2) Changement uniquement en mémoire (pas encore synchronisé avec la base)
p.setName("Draft name");
// 3) On réinitialise le persistence context : Hibernate oublie les changements accumulés
entityManager.clear();
// 4) On relit depuis la base : on obtient l'ancienne valeur, car l'UPDATE SQL n'a pas été envoyé
Product reread = entityManager.find(Product.class, 1L);
En résultat, reread.getName() retournera l'ancien nom depuis la base, et c'est absolument honnête. Aucune magie : tu n'as pas «perdu des données», tu as perdu l'état non synchronisé du persistence context.
Je souligne à part : clear() n'est pas une opération sur la base, mais sur le contexte courant. Si tu le perçois comme «recharger les données depuis la base», tu tireras presque certainement de mauvaises conclusions. Pour «recharger», il y a un autre outil — refresh(), et pour «pousser» — flush().
4. Détection précoce des erreurs de base via flush()
Voici maintenant la raison la plus pratique pour laquelle flush() est apprécié en diagnostic : il force le SQL à partir en base maintenant, et donc la base peut vérifier les contraintes maintenant. C'est utile quand la méthode de service est longue, comporte de nombreuses étapes, et qu'on ne veut pas apprendre une erreur d'unicité ou de NOT NULL seulement tout à la fin au commit, quand «tout s'est déjà passé» et que l'endroit de l'erreur est dilué.
Dans notre projet, il y a au moins un candidat évident : l'unicité de Product.sku. Si tu crées accidentellement deux produits avec le même SKU dans une seule transaction, jusqu'au flush tout peut sembler «normal» : tu as deux objets Java, tous les deux managed, tous les deux bien propres. Mais PostgreSQL à un moment dira : «Stop, violation d'unicité».
Avec un flush explicite, tu peux voir ça au plus près de l'endroit où tu as créé le problème :
import jakarta.persistence.EntityManager;
// 1) On crée et persiste le premier produit dans le persistence context
Product a = new Product();
a.setSku("SKU-123");
entityManager.persist(a); // l'INSERT SQL peut être reporté au flush/commit
// 2) On crée un deuxième produit avec le même SKU (le problème ne doit pas forcément se manifester tout de suite)
Product b = new Product();
b.setSku("SKU-123");
entityManager.persist(b);
// 3) On envoie le SQL en base de force : ici la base vérifiera UNIQUE et peut lever une exception
entityManager.flush(); // ici une constraint violation est probable
Le but n'est pas de «soigner» l'unicité avec flush, mais d'obtenir le signal plus tôt et de localiser l'erreur. En débogage, c'est très pratique : tu mets un flush() après un bloc potentiellement dangereux (création d'entité, création d'association, modification d'un champ important) et tu vois immédiatement — la base est d'accord ou non.
Il y a encore une nuance importante : si une erreur se produit au flush, ça ne signifie pas que «une partie des données a réussi à se sauvegarder». Dans la plupart des cas, la transaction sera marquée rollback-only et tout sera annulé. Mais pour le diagnostic, tu gagnes l'essentiel : tu attrapes le problème non pas «à la sortie de la méthode», mais presque à l'endroit où il a été créé.
5. Vérification via une requête en base après flush()
Parfois dans des scénarios pédagogiques et de débogage, on voudrait faire quelque chose du genre : «J'ai changé le nom du produit, et maintenant à l'intérieur de cette même transaction, je veux vérifier que dans la table product le nouveau nom est déjà là». Oui, Hibernate en mode AUTO fera souvent un flush avant une requête chevauchante, mais se reposer sur «souvent» est un mauvais style en diagnostic. Si tu veux que le SQL soit certainement déjà parti, fais-le explicitement et lis le résultat l'esprit tranquille.
Par exemple, tu veux après avoir changé le nom vérifier combien de lignes correspondent déjà à la nouvelle valeur :
import jakarta.persistence.EntityManager;
// 1) On lit l'entité et on la modifie en mémoire
Product p = entityManager.find(Product.class, 1L);
p.setName("Phone Pro");
// 2) On garantit que l'UPDATE a déjà été envoyé en base (dans le cadre de la transaction courante)
entityManager.flush();
// 3) On fait une requête directement sur la table, pour «interroger la base»,
// et non récupérer la valeur depuis le persistence context
Number count = (Number) entityManager
.createNativeQuery("""
select count(*)
from product
where name = 'Phone Pro'
""")
.getSingleResult();
// 4) À l'intérieur de la transaction, PostgreSQL voit les changements après flush
System.out.println("count = " + count); // on attend, par exemple : count = 1
On utilise ici intentionnellement du SQL natif pour «interroger directement la table», sans passer par le persistence context. Dans le cadre de la transaction, PostgreSQL voit déjà les changements envoyés, donc le résultat de la requête doit correspondre à ce qu'on vient d'écrire (à nouveau : tant que la transaction ne s'est pas rollbackée).
C'est un bon procédé quand on explore le comportement des flush modes. Par exemple, si tu as basculé le flush mode sur COMMIT ou MANUAL, une requête JPQL chevauchante peut se comporter différemment, et tu veux rendre le comportement maximal explicite : «d'abord flush, puis requête». Dans de telles expériences, flush() c'est comme une lampe puissante dans une pièce sombre : elle ne répare pas les meubles, mais te permet enfin de voir ce qui est vraiment de travers.
6. flush() pertinent et superflu
Après tout ça, on est facilement tenté de tomber amoureux de flush() et de le glisser tous les deux pas. Sur un petit exemple pédagogique, ça donne vraiment une agréable sensation de contrôle : on voit toujours le SQL, tout se passe «tout de suite». Mais ça a un coût. flush() n'est pas gratuit : il déclenche toute une procédure de flush, peut envoyer un lot de SQL, peut vérifier les contraintes trop tôt, peut déplacer l'ordre des opérations là où tu ne l'attendais pas, et en général rend l'exécution plus bruyante et plus lourde.
Le rapport sain à flush() : c'est un outil de contrôle ponctuel. Il est utile dans les labos, en diagnostic, à certains endroits du code où il faut voir une erreur de base plus tôt, et dans les tests où tu veux rendre l'état de la base observable avant la fin de la méthode. Mais si flush() devient un rituel obligatoire «après chaque changement», tu commences à te battre non contre le problème, mais contre ta propre méfiance envers le unit of work.
Un «marqueur» très simple de code sain : dans une méthode de service ordinaire, tu n'es généralement pas obligé d'appeler flush() manuellement. Hibernate fera lui-même un flush avant le commit et aux bons moments pour la cohérence des requêtes (selon le flush mode). Dans le code qui explore le comportement de l'ORM (notre cours est précisément sur ça), flush() est un excellent «commutateur de phase» qui rend les expériences reproductibles.
Et la dernière chose à redire, parce que le cerveau aime oublier : flush() ne remplace pas commit. Si tu as mis un flush() et qu'ensuite tu lèves une exception — tu n'as pas «sauvegardé à moitié», tu as juste réussi à envoyer le SQL dans le cadre d'une transaction qui a ensuite été annulée. C'est normal, c'est attendu, et c'est l'une des sources d'erreurs de déduction les plus fréquentes chez les débutants.
7. Erreurs typiques avec flush()
Ici il est particulièrement utile de lister les erreurs les plus fréquentes, parce que flush() est traître : il ressemble à un «bouton sauvegarder», mais en fait c'est un «bouton envoyer le SQL maintenant». La plupart des problèmes viennent non pas d'Hibernate, mais du fait qu'on commence à lui attribuer ce qu'il n'avait pas promis. Voici un ensemble de pièges typiques, sur lesquels même les développeurs confirmés se font prendre s'ils n'ont pas regardé le SQL trace depuis longtemps.
Erreur n° 1 : percevoir flush() comme un commit.
Si après flush() tu vois du SQL dans le log, il est facile de décider que «les données sont définitivement sauvegardées». En réalité flush() a juste envoyé les commandes en base, mais la transaction peut encore se rollbacker. C'est particulièrement visible quand à la fin de la méthode tu lèves une exception et que tu vois que rien n'a changé en base — et c'est correct.
Erreur n° 2 : faire une «vérification depuis la base» sans clear() et croire le résultat.
Scène fréquente : le développeur a modifié l'entité, rappelle find(), voit la nouvelle valeur et conclut que la base a été mise à jour. Mais à l'intérieur d'une même transaction, find() peut retourner le même objet managed depuis le first-level cache. Si tu veux relire honnêtement depuis la base, il faut un clear() (ou un autre moyen d'éjecter la copie managed) avant la nouvelle lecture.
Erreur n° 3 : appeler clear() avant flush() et perdre les changements non synchronisés.
C'est une erreur subtile mais très coûteuse, parce qu'elle n'aboutit pas toujours à une exception — parfois elle «avale» juste ton travail. Les changements dans les objets managed ne vivaient que dans le persistence context. Tu as fait clear() — et tu as toi-même demandé à Hibernate de les oublier. La base, de son côté, n'a rien reçu, parce que le SQL n'a pas été envoyé.
Erreur n° 4 : transformer flush() en rituel «après chaque appel à un setter».
Un tel code apparaît généralement par peur : «au cas où Hibernate ne sauvegarderait pas». Mais Hibernate existe précisément pour sauvegarder automatiquement à la fin du unit of work. Un flush manuel constant alourdit l'exécution, complique le débogage de l'ordre des opérations SQL, et peut provoquer des erreurs de contraintes précoces là où elles auraient dû apparaître seulement à la finalisation de l'opération métier.
Erreur n° 5 : tirer des conclusions sans le SQL log et «deviner» le moment du flush.
flush() — c'est une question d'observabilité. Si tu ne regardes pas le SQL trace (ou au moins les statistiques Hibernate), tu penseras que le flush était «ici», alors qu'il était «là», et tout le diagnostic sera faussé. Dans notre cours, la bonne habitude c'est : tout débat sur flush se confirme par ce qui est vraiment parti vers PostgreSQL.
GO TO FULL VERSION