CodeGym /Cours /Hibernate deep-dive /Axes du dernier audit de persistance

Axes du dernier audit de persistance

Hibernate deep-dive
Niveau 30, Leçon 0
Disponible

1. Persistence audit : cadre de vérification

Un audit de persistance final ne consiste pas à « prendre une loupe et trouver où on a oublié fetch=LAZY ». C'est plutôt une inspection technique de voiture : on ne discute pas de la couleur de la carrosserie (code-style), on vérifie où va réellement le carburant (SQL), si les freins tiennent (concurrency) et si la roue ne se détache pas dans un virage (correctness). On commence toujours par un use case.

À ce stade, vous avez déjà décomposé séparément flush, lazy loading, merge, locking, bulk et fetching. L'audit final sert exactement à cesser de les traiter comme des sujets isolés : d'abord on choisit un hot path et un axe de risque, puis on cherche des red flags, ensuite on vérifie les soupçons par SQL et tests, et seulement après on décide quoi réparer en premier.

Le changement de mentalité clé ici est le suivant : nous vérifions le comportement du système sur un scénario concret, et non « la qualité des entités dans le vide ». Même une @Entity parfaite peut devenir source de problèmes si on l'utilise comme un read-model universel, qu'on la traîne à travers les couches et qu'on parcourt accidentellement son graphe dans des streams, logs ou lors de la sérialisation. Et inversement : parfois un morceau de repository « pas très joli » peut être tout à fait normal parce qu'il dessert clairement un seul hot path et est soutenu par un test.

Pour éviter que l'audit ne devienne un « je jette un coup d'œil aux repositories au hasard », nous introduisons des axes d'audit — dimensions de vérification distinctes. Dans notre cours, ils sont quatre et correspondent volontairement à ce que nous avons étudié pendant tout le mois :

Axe d'audit Ce que nous vérifions en substance Sur quoi nous regardons dans le code/SQL Exemple issu du Commerce Persistence Lab
Correctness Le système modifie et lit les données de manière prévisible et correcte state transitions, flush timing, merge, bulk side effects, soft delete visibility « renommer un produit », « suppression en masse » et lecture ultérieure dans la même transaction
Performance Le système exécute un nombre raisonnable de requêtes SQL et ne charrie pas de données inutiles query count, N+1, largeur du SELECT, join shape, batching/bulk vs boucle liste des commandes/produits, détail d'une commande, import/mise à jour en masse
Concurrency En cas d'écriture parallèle, les données ne sont ni perdues ni corrompues @Version, lock modes, limites de transaction, longueur de la section critique réservation des stocks InventoryItem
Mapping quality Le modèle de relations et le mapping ne créent pas de « mines » à l'exécution owning side, cascade, orphan removal, equality, id strategy, contraintes/index PurchaseOrder ↔ OrderItem, entité de liaison ProductCategoryAssignment

Un autre mot important qui va revenir constamment aujourd'hui est hot path. C'est le scénario le plus important/fréquent/coûteux par lequel commencer. Si vous essayez d'« auditer tout à la fois », vous finirez dans un état « j'ai vérifié 200 méthodes et je n'ai pas compris ce qui était important ». Un audit repose toujours sur des priorités et des preuves, pas sur de l'héroïsme.

2. Correctness : des modifications de données prévisibles

Correctness est l'axe le moins « romantique » : il ne concerne ni la vitesse ni l'esthétique. Il s'agit de garantir que les données en base correspondent à vos attentes et que ces attentes sont conformes aux règles métier. Les débutants se cassent souvent les dents ici, car l'ORM est capable de faire beaucoup de choses « tout seul », et tout semble sous contrôle… jusqu'à ce qu'on découvre qu'une mise à jour en masse a laissé des objets obsolètes dans le persistence context.

La question la plus simple d'un audit de correctness semble banale, mais elle sauve des projets : « quel est le résultat considéré comme correct pour ce use case ? ». Par exemple, « renommer un produit » signifie qu'à la fin de la transaction, un UPDATE product set name = ... where id = ... a bien été exécuté, et rien de superflu n'est parti. « Supprimer un produit » dans un modèle de soft delete signifie non pas un DELETE, mais la modification d'un indicateur/état et son masquage correct dans les lectures. « Mettre à jour une commande » signifie que nous n'avons pas perdu d'articles, ni créé de doublons, ni reçu un DELETE en cascade inattendu à cause d'un orphanRemoval malencontreux.

Regardons le morceau de « correctness » le plus basique, que vous savez déjà expliquer : une entité gérée modifiée sans save().

import org.springframework.transaction.annotation.Transactional;

@Transactional // Important : sans transaction, les modifications peuvent ne pas survivre jusqu'au flush/commit comme vous l'attendez
public void renameProduct(Long id, String newName) {
    // Charge l'entité gérée : les modifications seront ensuite suivies par le persistence context
    Product product = productRepository.findById(id).orElseThrow();

    // Modifie l'état en mémoire : l'UPDATE SQL apparaîtra plus tard (lors du flush/commit)
    product.setName(newName);
}

Du point de vue de l'audit de correctness, nous ne vérifions pas si la méthode nous « plaît ». Nous vérifions qu'il s'agit bien d'un flux de mise à jour gérée (managed update flow), qu'il y a une transaction, et que l'UPDATE apparaîtra exactement une fois, au bon moment. Si quelqu'un a ajouté saveAndFlush() « au cas où », l'audit se doit de poser la question : pourquoi ce flush forcé ? Est-il nécessaire pour la correctness (par exemple, les données doivent impérativement être écrites en base avant la requête suivante dans la même transaction) ou n'est-ce qu'une habitude qui ajoute du bruit ?

Dans l'axe correctness, une autre « classique » refait vite surface : les opérations en masse (bulk) et le persistence context obsolète. Le bulk est presque toujours fait pour la performance, mais l'audit de correctness doit demander : qu'arrive-t-il aux entités déjà chargées ?

import org.springframework.transaction.annotation.Transactional;

@Transactional
public ProductStatus staleAfterBulk(Long id) {
    // 1) Charge l'entité dans le persistence context — c'est désormais un objet géré
    Product product = entityManager.find(Product.class, id);

    // 2) Effectue un UPDATE en masse : il va directement en base et NE synchronise PAS les entités gérées déjà chargées
    productRepository.markDeleted(id);

    // 3) Lit un champ de l'objet géré : on obtient l'ancienne valeur (état obsolète)
    //    Les corrections habituelles : clear()/refresh(), transaction séparée, relecture, etc.
    return product.getStatus();
}

C'est un exemple où correctness et performance se tiennent littéralement la main et essaient de vous faire tomber face contre le clavier. Le bulk a mis à jour la ligne en base, mais l'objet géré product à l'intérieur du persistence context est resté dans son ancien état. Si après cela le code continue à prendre des décisions sur la base de product, vous aurez un comportement logiquement incorrect. Nous avons déjà discuté des corrections dans le module sur les bulk : clear(), refresh(), extraction du bulk dans une transaction séparée, relecture. Ce qui importe dans l'audit n'est pas de « deviner le bon remède », mais de détecter le risque lui-même et de le consigner comme finding.

L'audit de correctness pose aussi constamment des questions sur merge(). Si vous voyez qu'un service reçoit un DTO, construit une entité détachée et appelle entityManager.merge(dtoAsEntity), il faut s'arrêter et demander : est-ce vraiment sûr pour votre graphe ? Cela ne va-t-il pas créer des SELECT superflus, ramener des objets enfants inattendus, ou supprimer quelque chose via un orphan removal ? Dans le cours, nous avons souvent conclu que pour la plupart des scénarios métier, le style find + mutate est plus prévisible. En audit, cela ne devient pas un dogme, mais une question : pourquoi le flux merge a-t-il été choisi ici et comment sa correctitude est-elle prouvée ?

Enfin, la correctness aime surgir là où on ne l'attend pas : à la frontière de la transaction et du lazy loading. Même si « tout a fonctionné » en local, l'audit doit garder en tête la baseline du cours : open-in-view=false. Si la couche de données renvoie une entité vers l'extérieur et que la couche externe commence à parcourir les relations lazy après la transaction, c'est un bug de correctness déguisé en « simple exception ».

3. Performance : compter les requêtes SQL et voir le coût

L'audit de performance ne commence pas par « ça rame », mais par une simple habitude d'ingénieur : compter combien de requêtes et de quels types sont générées pour un use case. Les projets utilisant ORM meurent souvent non pas à cause de « SQL complexe », mais à cause d'un « nombre trop élevé de SQL simples » — des milliers de petits SELECT qui, pris individuellement, sont anodins, mais qui ensemble transforment votre processeur en four micro-ondes.

Le plus agréable dans l'audit de performance est qu'il devient facilement fondé sur des preuves : vous activez le sql-trace/stats, exécutez le scénario — et tout devient visible. Pas besoin de deviner à l'intuition, il suffit de regarder le journal. Mais il est important de garder la discipline : on n'optimise pas « l'application dans son ensemble », on optimise un scénario concret. La liste des produits dans l'admin, le détail d'une commande, l'import de données snapshot — ce sont des histoires différentes avec des coûts différents.

Un red flag classique pour la performance est : « commençons par charger toutes les entités, puis en Java on en extraira quelque chose ». Cela semble anodin, mais déclenche souvent du N+1 (ou au moins un overfetching).

import java.util.List;

public List<String> findOrderEmails() {
    // Problème potentiel : findAll() charge les commandes, puis on "se promène" sur les associations
    // Si customer est LAZY (ce qui est généralement le cas), un SELECT séparé peut se produire à chaque itération
    return orderRepository.findAll().stream()
            .map(order -> order.getCustomer().getEmail()) // N+1 potentiel sur customer
            .toList();
}

Dans l'audit de performance, vous n'êtes pas obligé de réécrire la méthode sur-le-champ. Vous devez consigner le symptôme et la preuve : combien de requêtes ont été envoyées, quel format SQL, où sont les selects secondaires. Et ensuite vous choisissez une solution selon la matrice : projection, EntityGraph, JOIN FETCH, batch fetching ou autre approche. Ce qui importe ici est de reconnaître le type de problème : « navigation cachée dans le graphe au sein d'un stream » est presque toujours une invitation à la fête N+1, et Hibernate, à ces fêtes, n'apporte pas de pizza mais des factures d'infrastructure.

L'audit de performance ne regarde pas seulement les lectures, mais aussi les écritures. Là, on trouve des favoris particuliers :

Si vous voyez une « opération de masse » sous forme de boucle sur des entités avec save() à chaque itération, l'audit pose la question : le batching est-il activé, la stratégie d'identifiant correspond-elle, y a-t-il flush()/clear() dans la boucle, le persistence context ne gonfle-t-il pas jusqu'à la taille d'un petit État ? Et si vous voyez une modification massive de données (par exemple, « réévaluer le catalogue »), l'audit demande : est-il vraiment nécessaire de le faire avec une boucle d'entités plutôt qu'avec une mise à jour en masse ? Et si c'est une mise à jour en masse, revenez à la correctness et vérifiez l'état obsolète. C'est un cycle normal, cela doit se passer ainsi.

Un autre marqueur important de performance est la largeur du résultat. Parfois il n'y a « qu'une seule requête », mais elle ramène 25 colonnes et cinq jointures pour une liste qui a besoin de trois champs. Ce genre de choses se cache parfaitement derrière la phrase « au moins, pas de N+1 ». Dans votre tête d'auditeur, une idée simple doit habiter : une requête peut être pire que dix si elle est gonflée et matérialise un graphe géant.

4. Concurrency : écriture parallèle

L'audit de concurrency arrive presque toujours trop tard dans le projet : quand les données sont déjà perdues et que le métier a déjà demandé « pourquoi avons-nous moins trois iPhones en stock ? ». La bonne nouvelle est que notre projet pédagogique contient un chemin d'écriture contestée honnête : InventoryItem et la réservation des stocks. C'est un objet idéal pour l'entraînement : les mises à jour parallèles n'y sont pas une abstraction mais une situation normale.

La première question d'un audit de concurrency est brutale mais honnête : avons-nous une protection contre la lost update ? Si non, la « correctitude » du système dépend de la probabilité que les utilisateurs interviennent en même temps. Spoiler : en production, ils coïncident.

L'outil de base est le verrouillage optimiste via @Version. En audit, cela se présente de manière très simple : l'entité contestée a une version ou non.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;

@Entity
public class InventoryItem {
    @Id
    private Long id;

    @Version
    private long version; // La version est nécessaire pour détecter la lost update lors de modifications concurrentes

    private int availableQty; // Champ métier : stock disponible, autour duquel se produit généralement l'écriture contestée
}

Ensuite, l'audit vérifie que les opérations d'écriture vivent bien à l'intérieur d'une transaction raisonnable (courte et claire) et que le conflit est traité comme un événement attendu, non comme un « oups, 500 ». Nous n'entrons pas ici dans la conception de l'API, mais au niveau de la couche de données, il est important que le service puisse soit réessayer (si c'est acceptable), soit remonter honnêtement le conflit vers le haut.

Si le verrouillage optimiste ne convient pas (forte concurrence, coûteux de répéter), on passe au verrouillage pessimiste. Là, l'audit cesse d'être « une annotation » et devient « une question d'architecture » : à quel point la section critique est courte, y a-t-il un timeout, le verrou est-il maintenu pendant une longue logique métier. Dans ce cours, nous avons déjà fixé la règle : on ne doit pas maintenir un verrou de base de données à travers une interaction utilisateur et des étapes longues. L'audit de concurrency vérifie simplement que l'équipe vit bien selon cette règle et se contente pas de hocher la tête en cours.

Une subtilité particulière : les problèmes de concurrence ne sont presque jamais visibles dans des tests unitaires normaux, et même les tests d'intégration doivent reproduire le conflit de manière déterministe. Sinon, vous ne vérifiez pas la protection contre la lost update, vous jouez à la loterie.

5. Mapping quality : qualité du mapping

La qualité du mapping est généralement sous-estimée, car le mapping semble « statique » : une fois les annotations écrites, on oublie. Mais dans le monde d'Hibernate, la réalité est dure : ce que vous définissez dans le mapping détermine la forme des requêtes SQL, les cascades, l'ordre des vidages, le comportement des collections et même la sécurité de la journalisation des entités. Par conséquent, l'audit doit vérifier non seulement les « requêtes », mais aussi le « terrain sur lequel les requêtes poussent ».

L'approche la plus pratique dans l'audit de mapping consiste à demander : quelles règles du cycle de vie exprime ce mapping ? Si vous avez une relation bidirectionnelle, la correctitude dépend de la synchronisation des deux côtés. Si vous avez une cascade, elle doit exprimer la propriété (ownership), pas un « ALL au cas où ». Si vous avez un orphanRemoval, vous devez être certain que la rupture du lien signifie effectivement une suppression.

Un mini-exemple qui semble « trop simple » mais qui casse régulièrement des projets : une méthode helper sur l'agrégat de commande.

public void addItem(OrderItem item) {
    // Ajoute à la collection du côté de l'agrégat (généralement le côté inverse)
    items.add(item);

    // Obligatoire : synchroniser le côté propriétaire, sinon l'ordre des FK/flush peut se comporter de façon "étrange"
    item.setOrder(this);
}

Dans la logique d'audit, nous ne nous contentons pas de nous réjouir que la méthode existe. Nous vérifions que le code l'utilise réellement, qu'il n'y a pas d'appel direct order.getItems().add(item) dans les services, et que l'ordre de vidage et les clés étrangères ne conduisent pas à des UPDATE/NULL inattendus sur la clé étrangère. Le mapping n'est pas une décoration, c'est un contrat.

La qualité du mapping inclut également des aspects plus « subtils ». L'égalité/hashCode() avec proxy affecte les Set et l'orphan removal ; toString() peut déclencher un chargement paresseux ; la stratégie d'identifiant affecte le batching ; les contraintes et les index déterminent à quel point votre schéma protège les invariants (sku unique, orderNumber unique, la paire product+category unique). Tout cela fait partie de l'audit de mapping, même si cela ressemble à un « sujet de DBA ». Dans notre cours, nous en avons délibérément fait une partie du modèle, et non un « ajout ultérieur ».

6. Lien entre les axes et priorisation

Un problème — plusieurs axes : ne pas confondre les symptômes

La partie mature de l'audit commence quand vous cessez de ranger les problèmes dans de jolies boîtes. En réalité, une même erreur frappe souvent plusieurs axes à la fois. Et si vous ne la traitez qu'« à un endroit », elle revient sous une autre forme, comme un boomerang… sauf que le boomerang est en plus fâché.

Par exemple, un mauvais plan de chargement se manifeste généralement comme une douleur de performance (N+1, selects secondaires inutiles), mais il se transforme très facilement en douleur de correctness à la frontière de la transaction : « dans le service, on n'a pas tout chargé, à l'extérieur on a essayé de lire, on a eu une LazyInitializationException ». De même, une mise à jour en masse commence comme une solution de performance (« faisons un seul UPDATE au lieu d'un millier »), mais génère un risque de correctness d'état obsolète à l'intérieur du persistence context. Et l'absence de @Version ressemble à un problème de concurrency, mais le résultat est des données incorrectes (donc de la correctness).

Pour éviter de se mélanger, il est utile d'avoir en tête une courte « correspondance » :

Situation Avec quoi on la confond souvent Quels axes sont réellement impliqués
N+1 dans une liste « il faut mettre EAGER » performance + (souvent) mapping quality, parfois correctness (lazy boundary)
bulk update/delete « juste du SQL rapide » performance + correctness (stale state)
pas de @Version « eh bien on écrit rarement en parallèle » concurrency + correctness
mauvais owning side « Hibernate met à jour bizarrement » mapping quality + correctness (et parfois performance à cause d'UPDATE superflus)
saveAndFlush() « partout » « c'est plus sûr » correctness (flush timing) + performance (flush et SQL supplémentaires)

Le but de ce tableau n'est pas de l'apprendre comme une prière. Il s'agit de se poser deux questions à chaque finding : « quel axe était visible par le symptôme ? » et « quels axes sont réellement touchés ? ». Cela augmente nettement la qualité des décisions et réduit le risque de « réparer une chose — en casser une autre ».

Début de vérification : hot path et priorités

Le plus grand piège méthodologique du dernier jour est la tentative d'être un héros et de tout vérifier. Cela aboutit à trouver 30 remarques, à se fatiguer, et l'endroit le plus dangereux (par exemple, l'écriture concurrente des stocks) reste sans attention. C'est pourquoi l'audit commence non pas par une revue de code, mais par le choix d'un hot path — le scénario où l'erreur coûte le plus cher.

Un hot path n'est pas forcément le scénario le plus « joli ». C'est celui qui est soit fréquemment exécuté, soit critique pour les données, soit très coûteux en infrastructure. Dans notre Commerce Persistence Lab, les candidats typiques sont prosaïques : liste des produits dans l'admin (lecture fréquente), détail d'une commande avec ses articles (lecture complexe), réservation des stocks (écriture concurrente), bulk repricing (écriture en masse), lecture de l'historique des modifications (audit comme chemin séparé).

Ensuite, nous cherchons des red flags — des indicateurs de problèmes potentiels avant même une mesure précise. Ils ne prouvent pas une erreur, mais disent « il faut regarder le SQL ici ». Vous avez déjà vu des exemples de red flags des centaines de fois, simplement vous ne les appeliez pas ainsi : findAll() puis parcours du graphe ; EAGER par défaut ; saveAndFlush() dans un service sans explication ; une requête bulk puis un travail avec des entités déjà chargées ; absence de @Version sur une entité contestée ; modifications « directes » de collections sans méthodes helper.

La priorisation des findings dans une bonne équipe commence généralement par une règle simple : correctness et concurrency sur le hot path sont au-dessus de tout. Ensuite viennent les erreurs de performance coûteuses sur les chemins de lecture fréquents. Et seulement après viennent les « cosmétiques » (même si la cosmétique peut être utile, elle ne doit pas se substituer à la sécurité des données). Les red flags à eux seuls ne prouvent encore rien. Pour ne pas les traiter par habitude, il faut avoir une matrice de décision claire : quand une projection est appropriée, quand une entité + plan de chargement, quand un bulk, et quand du SQL natif. L'audit est une question d'ordre, pas de volume.

7. ReviewFinding : résultat de l'audit

Si vous ne consignez pas les résultats de l'audit dans un format reproductible, l'audit se transforme en conversation « il me semble que quelque chose ne va pas ici ». Et « il me semble » est un mot magnifique pour la poésie, mais très dangereux pour une couche de données. C'est pourquoi le niveau final introduit une discipline : chaque conclusion est formalisée en un finding, où sont enregistrés séparément le symptôme, la cause, la preuve, la décision et la priorité.

Le format peut être en Confluence, en Markdown ou en Jira — mais il est plus simple de raisonner avec une structure minimale. En Java, cela s'exprime parfaitement avec un record (et oui, c'est ce moment rare où un record aide non seulement à réduire le code mais aussi à forcer l'esprit à penser de manière structurée).

public record ReviewFinding(
        String area,      // Où se situe le problème : par exemple, catalog-read / inventory-write
        String symptom,   // Ce que l'on observe : "N+1 dans la liste des produits"
        String cause,     // Pourquoi cela se produit : "navigation lazy + chargement d'entité"
        String evidence,  // Avec quoi on le prouve : trace SQL, stats, requêtes spécifiques
        String decision,  // Ce que l'on fait : projection / EntityGraph / clear() après bulk etc.
        String priority   // Urgence : HIGH/MEDIUM/LOW (ou votre échelle)
) {}

Notez qu'il n'y a pas de champ « qui est responsable ». Hibernate ne s'offense pas, Spring Data non plus. On consigne une situation d'ingénierie. area peut être catalog-read ou inventory-write ; symptom — « N+1 dans la liste des produits » ; cause — « chargement d'entité pour la table + navigation lazy dans le mapper » ; evidence — « trace SQL : 1 + N requêtes » ; decision — « passer en projection » ; priority — « HIGH ».

Même un seul finding aide déjà l'équipe. Un ensemble de findings transforme l'audit en un plan d'amélioration gérable. Il est seulement important de ne pas perdre le dernier maillon : un finding sans preuve n'est encore qu'une hypothèse. Une trace SQL, des statistiques et un test de régression le transforment en fait d'ingénierie.

8. Erreurs typiques lors de l'audit de persistance final

Au dernier jour, il est facile de « retomber » dans de vieilles habitudes : disserter à nouveau sur les annotations, juger le code sur le style, traiter les symptômes avec des recettes universelles. C'est normal : le cerveau aime les réponses simples. Mais c'est justement pourquoi il est utile de connaître à l'avance les erreurs types et de se rattraper avant d'avoir mis un EAGER « pour éviter les lazy » et déclaré cela comme une victoire sur Hibernate.

Erreur n°1 : commencer l'audit par les classes d'entités, et non par un use case.
Si vous commencez par regarder les annotations, vous manquerez presque à coup sûr le vrai problème, car les problèmes vivent dans la chaîne « service → requête → transaction → SQL ». Une même entité peut être normale dans un scénario et horrible dans un autre. Commencez par un flux métier concret et déterminez ce qui est considéré comme un résultat correct pour celui-ci.

Erreur n°2 : confondre symptôme et cause.
« Nous avons beaucoup de requêtes » est un symptôme. « Nous avons du N+1 à cause d'une navigation cachée dans le graphe dans un stream/mapper » est une cause. « Nous utilisons un bulk update et ne vidons pas le persistence context » est une cause. Si vous notez immédiatement « solution : ajouter JOIN FETCH » sans avoir consigné la cause, vous risquez de faire un beau patch qui ne soigne pas la maladie ou la soigne au prix d'un autre problème.

Erreur n°3 : considérer tous les problèmes comme également importants.
Dans les projets réels, le temps pour les correctifs est toujours inférieur au nombre de problèmes. Si vous ne séparez pas les « risques pour les données » des « petites gênes », l'audit se transforme en une longue liste de remarques que personne n'implémentera. Prenez l'habitude : la correctness et la concurrency sur le hot path sont presque toujours prioritaires par rapport à l'optimisation locale d'un rapport rare.

Erreur n°4 : mener l'audit « à l'intuition », sans mesures.
La phrase « il me semble qu'il y a du N+1 » doit automatiquement déclencher l'étape suivante : activer la trace SQL/les statistiques, fixer une baseline et observer les faits. Le comportement d'un ORM est observable. Si vous ne regardez pas le SQL, vous discutez avec votre propre imagination, et celle-ci, en général, est très confiante et se trompe très souvent.

Erreur n°5 : commencer les corrections par une réécriture totale.
L'envie de « tout réécrire correctement » est compréhensible mais dangereuse. Un bon audit conduit presque toujours à des changements ciblés : remplacer le chargement d'entité par une projection dans une liste, ajouter @Version, supprimer un saveAndFlush() superflu, définir un plan de chargement explicite sur un détail, ajouter clear() après un bulk. Une grande réécriture sans preuves est souvent juste un moyen coûteux d'obtenir de nouveaux bugs.

1
Mission
Hibernate deep-dive, niveau 30, leçon 0
Bloqué
Carte des axes d'audit pour use case
Carte des axes d'audit pour use case
1
Mission
Hibernate deep-dive, niveau 30, leçon 0
Bloqué
Choix de l'axe de départ de l'audit
Choix de l'axe de départ de l'audit
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION