CodeGym /Cours /Spring Data JPA /Sélection de l'outil de requête dans Spring Data JPA

Sélection de l'outil de requête dans Spring Data JPA

Spring Data JPA
Niveau 16, Leçon 1
Disponible

1. Comment choisir l'outil de requête

Quand derived query, JPQL, projection, native query et Specification sont déjà connus individuellement, un autre problème apparaît : dans un même référentiel, ils se mélangent facilement sans aucune logique. Alors, toute manière de lire les données semble également bonne : « Ça marche, non ? ». Et c’est vrai… jusqu’au premier projet réel (ou jusqu’au second référentiel que vous avez ouvert une semaine plus tard et que vous n’avez pas reconnu). Dans Spring Data JPA, il existe plusieurs chemins parallèles pour un même résultat : derived query, JPQL, projection, native query, Specification. Si vous n’avez pas de règle simple de choix, vous commencerez inévitablement à tout mélanger – et le référentiel ressemblera à la cuisine d’un étudiant en fin de session : formellement, il y a de la nourriture, mais vivre là fait peur.

L’idée clé de cette leçon : le choix de l’outil de requête ne commence pas par « ce que je sais faire » ni par « ce qui est à la mode », mais par quel cas d’utilisation nous couvrons et quelle forme de résultat nous voulons obtenir. Autrement dit, nous formulons d’abord la question aux données, puis nous choisissons dans quelle langue poser cette question. Ce n’est pas une nouvelle syntaxe qu’il faut, mais une simple règle de choix.

Le cas d’utilisation et la forme du résultat priment sur la syntaxe

En général, l’erreur du débutant ressemble à ceci : il choisit d’abord l’outil, puis essaie d’y « fourrer » la tâche. C’est comme dans la vie : vous avez acheté une perceuse, et vous commencez à penser que tout dans le monde doit être percé. Dans la couche de données, c’est exactement la même chose : vous avez aimé les derived queries – vous commencerez à fabriquer des monstres derived ; vous avez appris le JPQL – vous écrirez des @Query pour un oui ou pour un non ; vous avez découvert le native SQL – et soudain, Spring Data JPA se transforme en « SQL dans les interfaces ».

Un démarrage plus sain consiste à se poser deux questions.

Première question : qu’est-ce que j’essaie exactement d’afficher ou de calculer ? Par exemple, « trouver un produit par SKU », « afficher une page de catalogue », « fournir une recherche admin avec des filtres optionnels », « construire un rapport sur les stocks ».

Deuxième question : sous quelle forme je veux renvoyer le résultat ? Parfois, on a besoin de l’entité Product complète (par exemple, pour une commande de changement de statut). Parfois, on a besoin de 3–4 champs pour une liste (nom, prix, catégorie) – c’est déjà un candidat direct pour projection. Parfois, on n’a besoin que d’un long (combien de produits actifs) – ce n’est pas du tout une histoire de lecture d’entités. Et c’est précisément la forme du résultat qui, souvent, vous pousse « naturellement » vers le bon outil.

Pour graver cela dans votre esprit, passons à des exemples tirés de mini-shop.

Si vous avez besoin de « trouver un produit par SKU pour modifier son prix », alors l’entité est un choix raisonnable : vous voulez vraiment un Product. Si vous avez besoin d’« afficher un catalogue de produits » où seuls name, price, categoryName sont montrés, alors traîner un Product complet avec la moitié des champs inutiles est étrange : vous construisez un modèle de lecture pour une liste, et non vous préparez à écrire.

Et ce n’est qu’après cela que vous choisissez le mécanisme : derived / JPQL / projection / native / Specification. Maintenant, examinons chaque outil « dans son habitat naturel ».

2. Derived queries : le nom de la méthode comme requête

Derived query – c’est le cas où Spring Data prend le nom de la méthode, le parse et génère lui-même la requête. Les premiers jours d’apprentissage, cela ressemble à de la magie du genre « j’ai écrit findByStatus et j’ai obtenu du SQL ». Mais en réalité, cette magie est tout à fait technique : les derived queries fonctionnent très bien tant que votre requête est simple, les conditions sont stables et le nom de la méthode reste lisible.

Dans le projet shop-data-jpa, l’approche derived est appropriée pour des choses comme « trouver les produits par statut », « trouver un produit par SKU », « compter le nombre de produits dans un statut ». Ce sont des scénarios courts, compréhensibles et répétables. Ici, le principe important est : derived query concerne une formulation simple de l’intention, et non une tentative de transformer le nom de la méthode en un langage de requêtes complet.

Exemple d’une méthode derived « saine » pour le catalogue :

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Derived query : Spring Data construit lui-même la requête à partir du nom de la méthode.
    // SKU est attendu unique, donc nous exprimons le résultat 0..1 via Optional.
    Optional<Product> findBySku(String sku);
}

Un autre cas classique – lorsque nous avons besoin d’un compteur plutôt que d’une liste :

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Retourne un scalaire (long), car le use case est "combien", et non "quoi exactement".
    long countByStatus(ProductStatus status);
}

Remarquez l’effet psychologique : par la seule signature, on voit à la fois l’intention et la forme du résultat. C’est ce qu’on appelle « l’auto-documentation », que nous recherchons.

La limite des derived queries apparaît environ là où vous vous surprenez à penser : « Bon, ça se lit… si on regarde longtemps et sans cligner des yeux ». Dès que le nom de la méthode devient plus complexe que la requête elle-même, derived cesse d’être un avantage. Ce n’est pas « mal », c’est simplement un signal : il est temps de changer d’outil.

3. JPQL et @Query : le texte de la requête plutôt qu’un roman

JPQL (Java Persistence Query Language) est un langage de requêtes basé sur le modèle entity, et non sur les tables. Il est pratique lorsque la méthode derived commence à gonfler et que vous avez besoin d’exprimer la condition explicitement. Ici, il est important de ne pas transformer JPQL en « une autre magie » : c’est un texte de requête ordinaire, simplement il parle le langage des entités.

La principale raison de choisir JPQL – expressivité et lisibilité. Oui, c’est une chaîne de caractères (et oui, les chaînes se cassent parfois lors du refactoring). Mais à un moment donné, la chaîne devient beaucoup plus honnête et plus courte qu’un nom derived long comme un paragraphe.

Exemple : nous avons besoin d’un extrait de catalogue « produits actifs avec un prix à partir de X », triés par nom. En style derived, cela pourrait déjà ne pas être très joli, alors que JPQL est compréhensible :

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // JPQL explicite : lisons la requête comme du texte et n'essayons pas d'« intégrer » la logique dans le nom de la méthode.
    // Les paramètres nommés sont plus faciles à lire et plus sûrs à maintenir que les paramètres positionnels.
    @Query("""
           select p
           from Product p
           where p.status = :status
             and p.price >= :min
           order by p.name
           """)
    List<Product> findCatalogChunk(@Param("status") ProductStatus status,
                                   @Param("min") BigDecimal min);
}

Remarquez deux points à prendre comme habitude. Nous utilisons des paramètres nommés :status et :min, car c’est plus facile à lire et plus difficile à confondre accidentellement. Nous utilisons une chaîne multiligne pour que la requête ressemble à un texte normal, et non à une « boulette de guillemets ».

Un autre signe que JPQL est adapté : vous avez besoin d’un join sur une association (par exemple, un filtre par catégorie). Les derived queries savent aussi naviguer dans les propriétés, mais JPQL rend souvent l’intention beaucoup plus claire :

@Query("""
       select p
       from Product p
       where p.category.id = :categoryId
         and p.status = :status
       """)
List<Product> findByCategoryAndStatus(
        @Param("categoryId") Long categoryId, // Filtrage par association (catégorie)
        @Param("status") ProductStatus status // Condition supplémentaire par statut
);

Pourquoi est-ce « plus honnête » ? Parce que le lecteur voit la requête de ses yeux et n’est pas obligé de décoder la « linguistique du nom de la méthode ». Et c’est particulièrement précieux quand le code vit plus d’une semaine (ce qui est généralement le cas, le rusé).

4. Projections : un modèle de lecture léger

Projections – c’est une manière de dire : « Je n’ai pas besoin de l’entité entière, donnez-moi seulement les champs nécessaires ». Et ce n’est pas un caprice. Dans le code backend réel, la moitié des lectures sont des listes et des vues résumées où l’on a besoin d’un modèle de lecture léger, et non d’un objet complet qui traîne potentiellement des détails superflus.

Important : la projection n’est pas un DTO « pour la couche web ». Nous sommes actuellement dans un cours sur la couche de données, et la projection y est un outil de modèle de lecture côté backend. Autrement dit : le modèle d’écriture est souvent orienté entité, tandis que le modèle de lecture est orienté cas d’utilisation.

Commençons par la projection d’interface (la plus simple). Pour le catalogue, nous pouvons avoir besoin d’une « ligne de liste » :

import java.math.BigDecimal;

public interface ProductCatalogRow {
    // Dans une projection d'interface, les méthodes accesseurs doivent correspondre aux alias/noms des champs.
    Long getId();
    String getName();
    BigDecimal getPrice();
}

Et maintenant, nous pouvons retourner non pas un Product, mais un ProductCatalogRow, même sur une méthode derived :

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Derived query + projection : le filtre est simple, et la forme du résultat est un « modèle de lecture léger ».
    List<ProductCatalogRow> findByStatus(ProductStatus status);
}

Du point de vue du lecteur du référentiel, c’est très soigné : le filtre est simple, derived est approprié, et la forme du résultat est déjà « légère ». Et c’est là que vous commencez à voir comment les outils se combinent, plutôt que de rivaliser.

Si vous avez besoin d’une structure plus explicite (et que vous voulez l’obtenir comme un vrai type Java, pas comme une « interface avec des getters »), vous pouvez utiliser une projection record :

import java.math.BigDecimal;

// Record en tant que modèle de lecture : pratique, compact, on ne peut pas accidentellement « écrire des setters ».
public record ProductPriceRow(Long id, BigDecimal price) {
}

Et le JPQL qui crée ce record via une expression constructeur :

@Query("""
       select new com.example.shopdatajpa.catalog.query.ProductPriceRow(p.id, p.price)
       from Product p
       where p.status = :status
       order by p.id
       """)
List<ProductPriceRow> findActivePrices(@Param("status") ProductStatus status); // On retourne exactement le modèle de lecture, pas l'entité

Oui, la chaîne est devenue un peu plus longue à cause du nom complet de la classe. Mais le contrat est totalement transparent : vous savez exactement ce qui sera retourné, et vous ne traînez pas de superflu.

La limite des projections se situe généralement là où vous commencez à les confondre avec des entités. La projection est un modèle de lecture, elle n’est pas destinée à être modifiée et sauvegardée. Si vous vous surprenez à vouloir faire productCatalogRow.setPrice(...) – cela signifie que vous êtes émotionnellement revenu à une pensée DTO, et la projection a cessé de jouer son rôle.

5. Native query : quand le SQL est vraiment nécessaire

Native query – c’est « parler à la base dans sa langue maternelle », c’est-à-dire en SQL. C’est puissant. Et c’est précisément pourquoi c’est dangereux comme habitude. Dans un projet sain, la native query est un outil ponctuel pour les cas où JPQL devient gênant, ou lorsque vous faites un rapport où la pensée SQL est plus naturelle.

Dans notre mini-shop, un exemple classique justifié est le rapport sur les stocks bas (low-stock report). Il ressemble souvent à un petit tableau de quelques champs, et l’écrire en SQL peut être vraiment plus simple et plus clair.

Créons une projection pour le rapport :

public interface LowStockRow {
    // Dans une requête native, il est particulièrement important que les noms correspondent aux alias (voir la requête ci-dessous).
    String getSku();
    Integer getAvailableQuantity();
}

Et la native query dans le référentiel des stocks :

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

import java.util.List;

public interface StockItemReportRepository extends Repository<StockItem, Long> {

    // Native SQL : on l'utilise quand une approche « SQL d'abord » est plus facile à lire que JPQL.
    // Les alias (as sku / as availableQuantity) rendent le mappage en projection prévisible.
    @Query(value = """
            select p.sku as sku, si.available_quantity as availableQuantity
            from stock_item si
            join product p on p.id = si.product_id
            where si.available_quantity < :threshold
            order by si.available_quantity asc
            """, nativeQuery = true)
    List<LowStockRow> findLowStock(int threshold); // threshold — paramètre d'entrée du rapport.
}

Remarquez deux disciplines. Premièrement, nous retournons non pas une entité, mais une projection de rapport : cela souligne le caractère de rapport de la lecture. Deuxièmement, nous utilisons des alias as sku et as availableQuantity pour que le mappage soit prévisible. Dans le SQL natif, la « discipline des alias » n’est pas une question d’esthétique, mais une manière d’éviter les surprises.

La native query se transforme particulièrement facilement en « usine à dette technique » si vous commencez à écrire du SQL pour tout. Dans ce cas, il ne reste de Spring Data JPA que le nom dans les dépendances, et le code migre vers « des chaînes dans les interfaces ». Donc la règle est simple : native query seulement quand il y a une véritable raison, pas quand « j’ai l’habitude ».

6. Specification : filtres dynamiques

Specification – c’est la réponse à la douleur typique de « recherche admin », où il y a beaucoup de filtres et chacun peut être spécifié ou non. Dans le monde derived, vous auriez une explosion combinatoire : findByStatusAndCategoryAndMinPriceAndMaxPriceAndNameContaining et ainsi de suite. Dans le monde JPQL, vous auriez une très longue requête avec plein de (:param is null or ...). Ça marche, mais la lisibilité en pâtit.

Specification permet d’assembler les conditions comme un constructeur, en les ajoutant soigneusement seulement lorsque le paramètre est réellement défini. Cela ne signifie pas qu’il faut toujours utiliser Specification. Cela signifie que Specification a son propre « habitat naturel » : les filtres dynamiques.

Dans le projet, cela ressemble généralement à ceci. D’abord, nous décrivons le filtre comme un simple record :

import java.math.BigDecimal;

// Paramètres d'entrée de la recherche : chaque champ peut être null (c'est-à-dire que le filtre n'est pas défini).
public record ProductAdminFilter(
        ProductStatus status,
        Long categoryId,
        BigDecimal minPrice,
        BigDecimal maxPrice,
        String text
) {
}

Ensuite, nous fabriquons un ensemble de « briques » de spécifications :

import org.springframework.data.jpa.domain.Specification;

import java.math.BigDecimal;

public final class ProductSpecs {

    private ProductSpecs() {
        // Classe utilitaire : les instances ne sont pas nécessaires
    }

    public static Specification<Product> status(ProductStatus status) {
        // Spécification « statut égal » : on l'utilise seulement si status != null (voir le service ci-dessous).
        return (root, query, cb) -> cb.equal(root.get("status"), status);
    }

    public static Specification<Product> priceGte(BigDecimal min) {
        // Spécification « prix >= min » : on l'utilise aussi seulement lorsque le paramètre n'est pas vide.
        return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get("price"), min);
    }
}

Et dans le service (ou le service de requête), nous assemblons la requête à partir des conditions réellement fournies :

import org.springframework.data.jpa.domain.Specification;

public class ProductAdminSearchService {

    public Specification<Product> buildSpec(ProductAdminFilter filter) {
        // Point de départ : spécification « vide » sans conditions.
        Specification<Product> spec = Specification.where(null);

        // Ajout des conditions seulement lorsque le paramètre est effectivement fourni.
        if (filter.status() != null) spec = spec.and(ProductSpecs.status(filter.status()));
        if (filter.minPrice() != null) spec = spec.and(ProductSpecs.priceGte(filter.minPrice()));

        return spec;
    }
}

Ici, nous ne discutons pas des subtilités de l’API et ne construisons pas de « constructeur de requêtes universel pour tous les cas ». L’idée est plus simple : Specification doit rester un outil pragmatique. Si vous avez deux conditions stables – derived ou JPQL seront plus simples. Si vous avez « cinq filtres, et n’importe lequel peut être vide » – Specification devient très appropriée.

7. Aide-mémoire pour le choix de l’outil

Dans la tête d’un débutant, tout cela peut ressembler à « encore cinq façons de tout compliquer ». C’est pourquoi nous n’avons pas besoin d’une revue encyclopédique, mais d’un schéma de choix simple. Imaginez que vous êtes dans un atelier : vous avez un tournevis, une clé, des pinces, un fer à souder et un marteau. On peut, bien sûr, ouvrir un pot de peinture avec un marteau. Mais il vaut mieux l’ouvrir avec un ouvre-boîte. Dans la couche de données, c’est exactement pareil.

Voici un tableau, comme un « aide-mémoire de choix ». Non pas comme un dogme, mais comme un repère.

Outil Quand l’utiliser Ce que l’on retourne généralement Principal avantage Principal inconvénient
Derived query Filtre simple, conditions stables, nom de méthode court et lisible Optional, List, parfois une projection ou un long Minimum de code, bonne intention Ne passe pas à l’échelle pour les cas complexes
JPQL (@Query) Le nom de la méthode devient un monstre ou besoin d’une requête explicite/join/logique List, projection, agrégats Lisibilité de la requête, expressivité La chaîne de la requête exige de la discipline
Projection Pour les listes/résumés où seule une partie des champs est nécessaire interface/record projection Lisez exactement ce dont le cas d’utilisation a besoin Ne pas confondre avec l’entité et ne pas « éditer »
Native query Rapports, spécificités du SGBD, lecture SQL complexe projection/scalaires Liberté maximale du SQL Coût de maintenance, discipline SQL
Specification Beaucoup de filtres optionnels, composition dynamique généralement List/résultat paginé, souvent + projection Pas d’explosion combinatoire de méthodes Complexité supplémentaire pour les cas simples

Maintenant, un mini-algorithme que l’on peut vraiment appliquer mentalement. Il est délibérément court et « humain ».

flowchart TD
    A["Un cas de lecture existe"] --> B{"Toute l'entité est nécessaire ?"}
    B -->|Oui| C{"Filtre simple et stable ?"}
    B -->|Non, une partie des champs| D["Projection"]
    C -->|Oui| E["Derived query"]
    C -->|Non| F{"Beaucoup de filtres optionnels ?"}
    F -->|Oui| G["Specification"]
    F -->|Non| H{"Rapport SQL-first / spécificité BD nécessaire ?"}
    H -->|Oui| I["Native query"]
    H -->|Non| J["JPQL @Query"]

Remarquez que la projection se trouve très proche du début. C’est intentionnel. Une « maladie infantile » fréquente est de lire l’entité par défaut tout le temps. Alors que le monde réel demande souvent des « vues », c’est-à-dire des modèles de lecture légers.

8. Exemples de choix dans mini-shop

Pour que la règle ne reste pas au niveau d’un « beau schéma », passons en revue plusieurs scénarios dans le style « quoi choisir et pourquoi », directement dans le domaine de mini-shop.

Si la question métier est « trouver un produit par SKU » – c’est pratiquement un derived query idéal. Il est court, compréhensible, et vous attendez 0 ou 1 résultat. Inutile d’écrire du JPQL : derived semble plus propre.

Si la question est « afficher une page de catalogue où, dans la ligne du produit, seuls name et price sont nécessaires » – c’est un candidat pour une projection. Et là, vous avez deux options : soit derived + projection (si le filtre est simple), soit JPQL + projection (si le filtre est plus complexe). Le sens est le même : vous adaptez le modèle de lecture à la liste.

Si la question est « recherche admin : statut optionnel, catégorie optionnelle, plage de prix optionnelle, texte optionnel » – alors Specification gagne très rapidement face à derived (trop de combinaisons) et face à JPQL (trop de if param is null). Vous décrivez chaque condition une fois, et vous assemblez la requête comme vous assemblez des Legos.

Si la question est « rapport sur les stocks bas, nécessite un join de tables et un tri/des conditions particuliers, plus faciles à exprimer en SQL » – alors native query devient un choix normal. Mais c’est précisément un scénario de rapport, localisé. Vous ne migrez pas toute l’architecture vers une approche SQL-first.

Et enfin, si la question est « logique complexe, mais il s’agit toujours d’une lecture selon le modèle d’entité, sans nécessité de spécificités SQL » – JPQL reste le juste milieu. Il permet d’exprimer explicitement la requête, mais vous maintient dans le monde du modèle d’entité (donc vous dépendez moins des détails du schéma des tables).

Tout ce tableau est important aussi parce qu’il protège le référentiel d’un « zoo stylistique ». Si l’équipe a convenu que les filtres simples se font en derived, les complexes en JPQL, les listes en projection, la dynamique en Specification, les rapports en native, alors les référentiels deviennent prévisibles. La prévisibilité est un super-pouvoir qui, curieusement, est rarement apprécié au début. Ensuite, on l’apprécie beaucoup. Parfois même en pleurant.

Et dès que cette matrice devient claire, la confusion typique suivante ne concerne plus le choix entre @Query et projection. Elle survient dans les verbes de base du référentiel : où le code voulait lire une entité, où prendre une référence, où simplement vérifier l’existence.

9. Erreurs typiques lors du choix de l’outil de requête

Lorsque vous avez cinq façons de lire les données, la main tend naturellement à faire « comme ça vient ». C’est normal : le cerveau est paresseux et veut répéter ce qui a déjà fonctionné. Mais ces habitudes ont des pannes caractéristiques.

Erreur n°1 : « Je retourne toujours une entité, parce que c’est plus simple ».
Au début, c’est effectivement plus simple, mais ensuite vous vous mettez soudainement à lire beaucoup de données là où une courte liste serait nécessaire. Le code gonfle, le contrat de lecture se dilue, et votre côté lecture cesse d’être orienté cas d’utilisation. L’habitude de considérer la projection comme un modèle de lecture normal guérit cela presque immédiatement.

Erreur n°2 : derived query comme « langage de requête universel ».
Quand vous transformez les noms de méthodes en romans, vous payez non pas en performance, mais en maintenabilité. Ce qu’il y a de plus vexant, c’est qu’un mois plus tard, vous avez vous-même du mal à comprendre ce qui est écrit, et l’IDE se transforme en « traducteur-assistant de votre propre langage vers l’humain ». Dès que le nom a cessé d’être lisible – ce n’est pas « encore un peu de patience », c’est un signal pour changer d’outil.

Erreur n°3 : JPQL pour le moindre prétexte « parce que j’ai appris @Query ».
JPQL est utile, mais c’est une chaîne, donc elle exige de la discipline : paramètres nommés, formatage lisible, intention compréhensible. Écrire @Query là où un derived serait plus simple – c’est aussi une forme de dette technique, simplement plus « intelligente ».

Erreur n°4 : native query comme style par défaut.
Le SQL natif est un outil qui exige une vraie responsabilité SQL. Dès que vous commencez à écrire massivement des native queries pour des lectures ordinaires, vous transformez insensiblement Spring Data JPA en une enveloppe coûteuse autour de chaînes SQL. C’est rarement ce que vous voulez dans une application où l’ORM a été choisi comme approche de base.

Erreur n°5 : Specification pour un seul filtre.
Specification concerne la dynamique et la composition. Si vous avez un ou deux paramètres stables, Specification ressemblera à « j’ai construit un vaisseau spatial pour aller chercher du pain ». Ça marche, mais cela soulève des questions chez les autres. Il vaut mieux garder Specification comme un outil pour les endroits où elle sauve réellement d’une explosion combinatoire.

1
Mission
Spring Data JPA, niveau 16, leçon 1
Bloqué
Derived pour un filtre simple et JPQL pour une requête explicite
Derived pour un filtre simple et JPQL pour une requête explicite
1
Mission
Spring Data JPA, niveau 16, leçon 1
Bloqué
Rapport de stock bas via une requête native et une projection
Rapport de stock bas via une requête native et une projection
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION