Accéder au contenu principal

Quand JPA rencontre ses limites, pensez à jOOQ et au MULTISET

Une seule requête, un graphe d'objets complet — ce n'est pas de la magie, c'est du SQL.

La base du problème

Dans le monde JPA, quand on gère une relation un-à-plusieurs, le chargement LAZY par défaut semble élégant… mais cache une bombe à retardement.

Imaginez un scénario e-commerce classique : 

Client → Commandes → Lignes de commande → Produits. 

Quand vous appelez orderRepository.findAll(), si Open Session in View est activé (plus de 701 allers-retours vers la base de données : 1 requête pour les commandes, 100 pour les clients, 100 pour les lignes, 500 pour les produits.

En dev, tout peut bien aller, mais en production il y a risque d'effondrement des performances.

Ce n'est pas la faute d'Hibernate. C'est le prix inévitable du modèle de navigation par objets face à des hiérarchies complexes.


Limite du join fetch de JPA

Beaucoup pensent résoudre le N+1 avec JOIN FETCH. Mais il y a un mur contre lequel tout le monde finit par se cogner : on ne peut pas chaîner plusieurs JOIN FETCH sur des collections. Hibernate lève une MultipleBagFetchException. 

Les contournements habituels sont tous imparfaits :

  • Passer les List en Set : ça marche, mais ça impact les performances (pas d'ordre garanti, pas de List sémantique) et ça masque le problème sans le résoudre

  • Utiliser @BatchSize ou @Fetch(FetchMode.SUBSELECT) : ça réduit le N+1 mais ça ne le supprime pas, et ça reste hors de contrôle du développeur

  • Forcer FetchType.EAGER : c'est la porte ouverte aux explosions de performances ailleurs dans l'application

  • Découper en plusieurs requêtes manuelles : retour à la case départ, on réintroduit la logique de jointure à la main

C'est à ce moment que le Multiset de jOOQ devient intéressant.

jOOQ MULTISET : une requête, trois niveaux d'imbrication

Le constructeur MULTISET introduit par jOOQ  permet de récupérer et mapper des données hiérarchiques dans une seule requête SQL, sans aucune limite sur le nombre de collections imbriquées.


Définition du dto

public record ProductDTO(Long id, String name, double price) {}
public record OrderItemDTO(Long id, int quantity, ProductDTO product) {}
public record PurchaseOrderDTO(Long id, LocalDateTime orderDate, 
                               CustomerDTO customer, List<OrderItemDTO> items) {}

La requête

ctx.select(
    PURCHASE_ORDER.ID,
    PURCHASE_ORDER.ORDER_DATE,
    row(PURCHASE_ORDER.customer().ID, 
        PURCHASE_ORDER.customer().FIRST_NAME)
        .mapping(CustomerDTO::new),
    multiset(
        select(ORDER_ITEM.ID, ORDER_ITEM.QUANTITY,
               row(ORDER_ITEM.product().ID, 
                   ORDER_ITEM.product().NAME, 
                   ORDER_ITEM.product().PRICE)
                   .mapping(ProductDTO::new))
        .from(ORDER_ITEM)
        .where(ORDER_ITEM.ORDER_ID.eq(PURCHASE_ORDER.ID))
    ).convertFrom(r -> r.map(OrderItemDTO::new))
).from(PURCHASE_ORDER).fetch(PurchaseOrderDTO::new);


jOOQ ne remplace pas JPA

Ces outil sont complémentaires

Une architecture possible

  • Écritures avec JPA : entités métier, règles de gestion
  • Lectures complexes avec jOOQ : reporting, DTO imbriqués, agrégations multi-niveaux

Après il est tout à fait possible d'utiliser du SQL natif pour les cas les plus exigeant.

Commentaires

Posts les plus consultés de ce blog

Toshiba AC100 et Android 4

Toshiba AC100 est un smartbook. Toshiba est la seule grosse entreprise qui a commercialisé un smartbook, malgré que plusieurs autres en aient présenté. Un smartbook est similaire à un netbook (taille) en plus d'avoir des caractéristiques souvent proche à un mobile (GPS, 3g, processeur ARM, autonomie). Le Toshiba AC100 utilisait le système d'exploitation Android en version 2.1. Toshiba a effectué une mise à jour Android 2.2. Il a arrêté la commercialisation et le support de cette machine. L'appareil ayant un bon rapport qualité/prix, la communauté le prit en charge. Il est possible d'installer une version beaucoup plus récente d'Android telle que la 4.1. La rom alternative Cyanogen a été utilisée pour ce portage. Les procédures citées ont été faites sous Linux.  Sous Windows, il faut télécharger une version de nvflash pour ce système. Préparation Télécharger les fichiers: cm_ac100-ota-eng.cm10-beta2.zombah.zip recovery.ac100-cm10_beta2.img via ce...

Spring data jpa et Hibernate’s Warning “firstResult/maxResults specified with collection fetch”

Nous verrons dans cet article comment corrigé le problème  Hibernate’s Warning “firstResult/maxResults specified with collection fetch” qui peut survenir lorsqu'il y a un fetch d'effectuer et qu'il y a du paging. Nous utiliserons spring data jpa. Détail structure Imaginons une relation 1 à plusieurs Lorsqu'on obtient un parent, on désire obtenir aussi sa liste d'enfants. En sql, il y aurait une jointure à effectuer. Il est question d'une recherche avancé où on pourrait recherche par plusieurs critères. Spécification Création d'une spécification pour construire une requête dynamique @Component public class ParentSpecification {     public Specification<Parent> recherche(RechercheParent recherche) {         return (root, cq, cb) -> {             Join<Parent, enfant> enfantJoin;              List<Predicate> rootPredicates = new ArrayList<>();       ...

Migration d’une application Spring Boot 2.2 vers Spring Boot 4.0 : retour d’expérience concret

 En 2019, nous avons développé une application métier basée sur : Java 8 Spring Boot 2.2.7 Gradle 6.6.1 Thymeleaf Bootstrap 4.3.1 Environ 40 000 lignes de code Java Fin 2025, voici un retour d’expérience concret sur la migration vers : Java 25 Spring Boot 4.0.1 Gradle 9.2.1 La partie thymeleaf, n'a pas été touché, il y a quelques warning concernant l'obsolescence de certain commande tel que th:include. Une migration plus rapide que prévu Un point important : en moins de 30 minutes, l’application démarrait sur la nouvelle stack. Évidemment, "ça démarre" ne veut pas dire "tout fonctionne", mais ce premier jalon est très révélateur : la base Spring Boot reste solide les outils de migration sont matures la dette technique était maîtrisée Les changements majeurs rencontrés De javax.* à jakarta.* C’est probablement le changement le plus visible. Tous les imports javax.* doivent être migrés ve...