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
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
ListenSet: ça marche, mais ça impact les performances (pas d'ordre garanti, pas deListsémantique) et ça masque le problème sans le résoudreUtiliser
@BatchSizeou@Fetch(FetchMode.SUBSELECT): ça réduit le N+1 mais ça ne le supprime pas, et ça reste hors de contrôle du développeurForcer
FetchType.EAGER: c'est la porte ouverte aux explosions de performances ailleurs dans l'applicationDécouper en plusieurs requêtes manuelles : retour à la case départ, on réintroduit la logique de jointure à la main
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émentairesUne 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