Aller au contenu

Feature - Chien (Partie 2 : listing, pagination, recherche avancée)

Pourquoi cette partie ?

Créer des données c’est bien, les retrouver c’est mieux !

Un internaute veut :

On va mettre en place : Pageable + Specification.


Quelques explications

Voici un exemple d’écriture que nous avons un peu plus bas :

// on est dans le serviceChien
// important, il faut injecter le ChienMapper
 private final ChienMapper chienMapper;
 public ServiceChien(ChienRepository chienRepository, ChienMapper chienMapper)
	{
		this.chienRepository = chienRepository;
		this.chienMapper = chienMapper;
	}
public Page<ChienDto> list(Pageable pageable) {
  return chienRepo.findAll(pageable).map(chienMapper::toDto);
}

Il faut importer les packages suivants :

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

Dans cette méthode, Pageable est un objet Spring qui permet de gérer :

Lors de la récupération des données depuis une base de données.

Voici une explication détaillée de son fonctionnement dans ce contexte :

Rôle de Pageable

Pageable est une interface Spring qui encapsule les informations nécessaires pour paginer les résultats d’une requête :

Comment ça marche ?

Étape 1 : Appel de findAll(Pageable pageable)

Pour notre chienRepo.findAll(pageable) :

Étape 2 : Transformation avec map(mapper::toDto)

Pour notre .map(mapper::toDto) :

Supposons que :

La base de données contient 50 chiens et que vous appelez cette méthode avec un Pageable configuré comme suit :

Ce qui se passe :

Avec chienRepo.findAll(pageable) :

Exécute une requête SQL du type : SELECT * FROM chien ORDER BY id ASC LIMIT 10 OFFSET 0

Que fait .map(mapper::toDto) ?

Comment utiliser Pageable dans votre contrôleur ?

Pour utiliser cette méthode dans un contrôleur Spring, vous pouvez injecter Pageable comme paramètre. Spring le construit automatiquement à partir des paramètres de la requête HTTP.

Exemple de contrôleur :

@RestController
@RequestMapping("/api/chiens")
public class ChienController {

    private  final ChienService chienService;

    public ChienController(ChienService chienService) {
      this.chienService = chienService;
    }

    @GetMapping
    public Page<ChienDto> listChiens(
        @PageableDefault(size = 3, sort = "id", direction = Sort.Direction.ASC) Pageable pageable) {
        return chienService.list(pageable);
    }
}

Requête HTTP pour récupérer la 2ème page (3 chiens par page, triés par id ascendant) :

```bash
GET /api/chiens?page=1&size=10&sort=id,asc

Avantages de cette approche

Structure de l’objet Page retourné

L’objet Page<ChienDto> contient :

Exemple de réponse JSON :

{
  "content": [
    {"id": 1, "nom": "Rex", ...},
    {"id": 2, "nom": "Plume", ...},
    ...
  ],
  "pageable": {
    "sort": {"sorted": true, "unsorted": false, "empty": false},
    "pageNumber": 0,
    "pageSize": 10,
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 5,
  "totalElements": 50,
  "last": false,
  "first": true,
  "sort": {"sorted": true, "unsorted": false, "empty": false},
  "numberOfElements": 10,
  "empty": false
}

Pageable permet de paginer et trier les résultats côté base de données. Il encapsule les résultats paginés + des métadonnées utiles et map transforme les entités en DTOs tout en conservant les métadonnées de pagination. Bien pratique !

Listing paginé (Spring Data) détaillé (pas à pas)

Dans le controller :

@GetMapping
public Page<ChienDto> list(Pageable pageable) {
  return service.list(pageable);
}

Dans le service :

public Page<ChienDto> list(Pageable pageable) {
  return chienRepo.findAll(pageable).map(mapper::toDto);
}

ChienRepository ne doit contenir que des méthodes qui retournent des entités JPA (Chien). La conversion en DTO doit être faite dans le service, pas dans le repository, donc pas de méthode list() dans le repository.

Exemple URL

GET /api/chiens?page=0&size=10&sort=nom,asc

Exemples de requêtes et de résultats (sachant qu’il n’y a que 4 chiens dans la base):

Pageable Chien :

http://localhost:8088/api/public/rechercher?page=0&size=3&sort=id

http://localhost:8088/api/public/rechercher?page=0&size=3&sort=numeroTatouage

Méfiez-vous du format JSON dans Swagger, il va mettre sort au format tableau, modifié le JSON :

{
  "page": 0,
  "size": 3,
  "sort": "id"
}

Pourquoi pas List tout simplement ?

Une List de 200 000 chiens serait :

La pagination vue plus haut permet :


Exerice simple : Recherche par race

Repository :

Page<Chien> findByRaceId(Long raceId, Pageable pageable);

Service :

public Page<ChienDto> listByRace(Long raceId, Pageable pageable) {
  return chienRepo.findByRaceId(raceId, pageable).map(mapper::toDto);
}

Recherche avancée : Utilisation des Specifications

Pourquoi ?

Pratique quand les filtres à combiner : race + état + nom + propriétaire

Si on crée une méthode repository par combinaison :

On utilise JpaSpecificationExecutor.

Repository :

public interface ChienRepository extends JpaRepository<Chien, Long>, JpaSpecificationExecutor<Chien> {}

Qu’est-ce qu’une Specification ?

Une Specification est une interface de Spring Data JPA qui permet de construire dynamiquement des requêtes SQL en utilisant des critères.

En fait, elle ajoute les 3 méthodes ci-dessous :

findAll(Specification<T> spec)
findOne(Specification<T> spec)
count(Specification<T> spec)

Structure d’une Specification

Chaque méthode retourne un objet Specification<Chien>, qui est en réalité une fonction lambda prenant (root, query, cb) et retournant un Predicate (une condition SQL).

Exemples avec hasRaceId, hasRace et ageGreaterThan :

public static Specification<Chien> hasRaceId(Long raceId) {
  return (root, query, cb) ->
    raceId == null
      ? cb.conjunction()  // si raceId est null, retourne une condition "toujours vraie"
      : cb.equal(root.get("race").get("id"), raceId);  // sinon, ajoute la condition "race.id = raceId"
}
public static Specification<Chien> hasRace(String race) {
  return (root, query, cb) ->
      cb.equal(root.get("race"), race);
    }

public static Specification<Chien> ageGreaterThan(int age) {
  return (root, query, cb) ->
      cb.greaterThan(root.get("age"), age);
    }

Exemple d’utilisation :

// séparation parfaite des responsabilités
Specification<Chien> spec =
        ChienSpecifications.hasRace("Labrador")
            .and(ChienSpecifications.ageGreaterThan(3));

List<Chien> chiens = chienRepository.findAll(spec);

Détail de root.get(“race”).get(“id”) :

Explication de cb.equal(…) : crée une condition d’égalité (WHERE race.id = :raceId).

Que fait cb.conjunction() :

Explication de nameContains et la variable q

public static Specification<Chien> nameContains(String q) {
  return (root, query, cb) ->
    (q == null || q.isBlank())
      ? cb.conjunction()  // Si q est null ou vide, pas de filtre
      : cb.like(cb.lower(root.get("nom")), "%" + q.toLowerCase() + "%");  // Sinon, recherche partielle
}

Rôle de q :

Par exemple, si q = “rex”, la requête cherchera tous les chiens dont le nom contient “rex” (en minuscules).

Détail de la condition :

cb.lower(root.get("nom")) : convertit le nom du chien en minuscules pour une recherche case-insensitive.

cb.like(..., "%" + q.toLowerCase() + "%") :

Utilisation des Specifications

Ces Specifications sont généralement utilisées pour combiner dynamiquement des critères dans une requête.

Élément Rôle
JpaRepository Accès aux données
Specification<T> Filtre / requête
JpaSpecificationExecutor<T> Pont entre les deux

Spring permet (avec ce type d’écriture) :

Par exemple :

// dans un service ou un contrôleur
public List<Chien> searchChiens(Long raceId, EtatChien etat, String q) {
  Specification<Chien> spec = Specification.where(hasRaceId(raceId))
    .and(hasEtat(etat))
    .and(nameContains(q));
  return chienRepository.findAll(spec);
}

Exemple d’appel :

// chercher les chiens de race 1, avec l'état "ADOPTE" et dont le nom contient "rex"
List<Chien> chiens = searchChiens(1, EtatChien.ADOPTE, "rex");

Requête SQL générée (simplifiée) :

SELECT * FROM chien
WHERE
  (race_id = 1)  -- hasRaceId(1)
  AND (etat = 'ADOPTE')  -- hasEtat(EtatChien.ADOPTÉ)
  AND (LOWER(nom) LIKE '%rex%')  -- nameContains("rex")

Pourquoi cb.conjunction() ?

Il retourne une condition toujours vraie comme si on utilisait un WHERE 1=1 :

Cela permet de ne pas appliquer de filtre si le paramètre est null ou vide, tout en gardant une syntaxe cohérente pour combiner les Specifications.

Pratique : constructeur de specifications diverses

public class ChienSpecifications {

  public static Specification<Chien> hasRaceId(Long raceId) {
    return (root, query, cb) -> raceId == null ? cb.conjunction() : cb.equal(root.get("race").get("id"), raceId);
  }

  public static Specification<Chien> hasEtat(EtatChien etat) {
    return (root, query, cb) -> etat == null ? cb.conjunction() : cb.equal(root.get("etat"), etat);
  }

  public static Specification<Chien> nameContains(String q) {
    return (root, query, cb) -> (q == null || q.isBlank()) ? cb.conjunction()
      : cb.like(cb.lower(root.get("nom")), "%" + q.toLowerCase() + "%");
  }
}

Assemblage dans le service

Vous constatez que la construction de requêtes spécifiques devient facile.

public Page<ChienDto> search(Long raceId, EtatChien etat, String q, Pageable pageable) {
  var specifications = Specification
    .where(ChienSpecifications.hasRaceId(raceId))
    .and(ChienSpecifications.hasEtat(etat))
    .and(ChienSpecifications.nameContains(q));

  return chienRepo.findAll(specifications, pageable).map(mapper::toDto);
}

Dans le contrôleur :

@GetMapping("/search")
public Page<ChienDto> search(
  @RequestParam(required=false) Long raceId,
  @RequestParam(required=false) EtatChien etat, // si vous l'avez fait sinon à ajouter (enum)
  @RequestParam(required=false) String q,
  Pageable pageable
) {
  return service.search(raceId, etat, q, pageable);
}

Exemples HTTP

GET /api/chiens/search?raceId=1&etat=INSCRIT&q=re&page=0&size=5

Pièges


A faire

  1. Ajoutez un tri par numeroTatouage et couleurRobe avec une pagination.

Il faut juste ajouter plusieurs chiens dans la base pour la pagination en utilisant la classe SeedConfig. Peut-être utiliser Mockaroo…