🖨️ Version PDF
Créer des données c’est bien, les retrouver c’est mieux !
Un internaute veut :
On va mettre en place : Pageable + Specification.
Pageable
Specification
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 :
Pageable est une interface Spring qui encapsule les informations nécessaires pour paginer les résultats d’une requête :
Étape 1 : Appel de findAll(Pageable pageable)
findAll(Pageable pageable)
Pour notre chienRepo.findAll(pageable) :
chienRepo.findAll(pageable)
Page<Chien>
Étape 2 : Transformation avec map(mapper::toDto)
Pour notre .map(mapper::toDto) :
.map(mapper::toDto)
mapper::toDto
Page<ChienDto>
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) ?
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
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 !
map
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.
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" }
List
Une List de 200 000 chiens serait :
La pagination vue plus haut permet :
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); }
Pratique quand les filtres à combiner : race + état + nom + propriétaire
Si on crée une méthode repository par combinaison :
On utilise JpaSpecificationExecutor.
JpaSpecificationExecutor
public interface ChienRepository extends JpaRepository<Chien, Long>, JpaSpecificationExecutor<Chien> {}
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)
CriteriaBuilder
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).
Specification<Chien>
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”) :
race.id
Explication de cb.equal(…) : crée une condition d’égalité (WHERE race.id = :raceId).
Que fait cb.conjunction() :
cb.conjunction()
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.lower(root.get("nom"))
cb.like(..., "%" + q.toLowerCase() + "%") :
cb.like(..., "%" + q.toLowerCase() + "%")
Ces Specifications sont généralement utilisées pour combiner dynamiquement des critères dans une requête.
JpaRepository
Specification<T>
JpaSpecificationExecutor<T>
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")
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.
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() + "%"); } }
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); }
GET /api/chiens/search?raceId=1&etat=INSCRIT&q=re&page=0&size=5
lower()
null
numeroTatouage
couleurRobe
Il faut juste ajouter plusieurs chiens dans la base pour la pagination en utilisant la classe SeedConfig. Peut-être utiliser Mockaroo…
SeedConfig