Aller au contenu principal

Chapitre 4: Pagination et Performance

Introduction​

Problème: Vous avez 100,000 produits en base. Vous les affichez tous?

List<Product> products = productRepo.findAll();  // ❌ 100,000 objets en mémoire!

Résultat:

  • 💥 OutOfMemoryError
  • 🐢 Application très lente
  • 😡 Utilisateurs mécontents

Ce chapitre vous apprend à optimiser!

Ce que vous allez apprendre​

  1. Pagination avec Pageable et Page<T>
  2. Tri avec Sort
  3. Le problème N+1 et comment le résoudre
  4. Projections pour la performance

Durée: 1h


Pagination avec Pageable​

Le Concept​

Au lieu de charger tout:

Page 1: [Produits 1-20]
Page 2: [Produits 21-40]
Page 3: [Produits 41-60]
...

L'utilisateur voit 20 résultats à la fois, pas 100,000!


Code: Repository avec Pagination​

public interface ProductRepository extends JpaRepository<Product, Long> {

// Méthode héritée
Page<Product> findAll(Pageable pageable);

// Votre méthode avec pagination
Page<Product> findByCategory(String category, Pageable pageable);

// Avec @Query
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
Page<Product> findCheap(@Param("maxPrice") Double maxPrice, Pageable pageable);
}

Points clés:

  • Paramètre Pageable en dernier
  • Retour Page<T> (pas List<T>)

Utilisation: Créer un Pageable​

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

// Page 0 (première page), 20 éléments par page
Pageable pageable = PageRequest.of(0, 20);
Page<Product> page = productRepo.findAll(pageable);

// Informations de la page
int totalPages = page.getTotalPages(); // Nombre total de pages
long totalElements = page.getTotalElements(); // Nombre total d'éléments
List<Product> products = page.getContent(); // Les 20 produits de la page
boolean hasNext = page.hasNext(); // Y a-t-il une page suivante?

Exemple Complet: API REST Paginée​

Controller:

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductRepository productRepo;

public ProductController(ProductRepository productRepo) {
this.productRepo = productRepo;
}

@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(page, size);
return productRepo.findAll(pageable);
}
}

Requête:

GET /api/products?page=0&size=20

Réponse JSON:

{
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" },
...
],
"pageable": {
"pageNumber": 0,
"pageSize": 20
},
"totalPages": 500,
"totalElements": 10000,
"last": false,
"first": true
}

Tri avec Sort​

Tri Simple​

import org.springframework.data.domain.Sort;

// Tri par prix ascendant
Sort sort = Sort.by("price").ascending();
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20, sort));

// Tri par prix descendant
Sort sort = Sort.by("price").descending();

Tri Multiple​

// Tri par catégorie ASC, puis prix DESC
Sort sort = Sort.by("category").ascending()
.and(Sort.by("price").descending());

Pageable pageable = PageRequest.of(0, 20, sort);
Page<Product> page = productRepo.findAll(pageable);

Tri dans le Controller​

@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "ASC") String direction
) {
Sort sort = direction.equalsIgnoreCase("DESC")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();

Pageable pageable = PageRequest.of(page, size, sort);
return productRepo.findAll(pageable);
}

Requête:

GET /api/products?page=0&size=20&sortBy=price&direction=DESC

Le Problème N+1​

Qu'est-ce que c'est?​

Le problème le PLUS FRÉQUENT de performance en JPA!


Exemple du Problème​

Entités:

@Entity
public class Department {
@Id
private Long id;
private String name;

@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;
}

@Entity
public class Employee {
@Id
private Long id;
private String name;

@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}

Code problématique:

List<Department> departments = deptRepo.findAll();  // 1 requête

for (Department dept : departments) {
List<Employee> employees = dept.getEmployees(); // N requêtes!
System.out.println(dept.getName() + ": " + employees.size());
}

SQL exécuté:

-- 1 requête
SELECT * FROM department;

-- N requêtes (une par département!)
SELECT * FROM employee WHERE department_id = 1;
SELECT * FROM employee WHERE department_id = 2;
SELECT * FROM employee WHERE department_id = 3;
...
SELECT * FROM employee WHERE department_id = N;

Si vous avez 100 départements → 1 + 100 = 101 requêtes! 💥


Solution 1: JOIN FETCH​

@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();

SQL exécuté:

-- 1 SEULE requête!
SELECT d.*, e.*
FROM department d
LEFT JOIN employee e ON d.id = e.department_id;

✅ 1 requête au lieu de 101!


Solution 2: @EntityGraph​

@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();

Équivalent à JOIN FETCH mais plus simple!


Solution 3: Batch Size​

Si JOIN FETCH n'est pas possible:

@Entity
public class Department {

@OneToMany(mappedBy = "department")
@BatchSize(size = 10) // Charge 10 départements à la fois
private List<Employee> employees;
}

SQL exécuté:

SELECT * FROM department;

-- Au lieu de N requêtes, seulement N/10!
SELECT * FROM employee WHERE department_id IN (1, 2, 3, ..., 10);
SELECT * FROM employee WHERE department_id IN (11, 12, 13, ..., 20);
...

Projections pour la Performance​

Problème​

Vous chargez TOUTE l'entité alors que vous n'avez besoin que de 2 champs:

List<Product> products = productRepo.findAll();
for (Product p : products) {
System.out.println(p.getName() + ": " + p.getPrice());
// Vous n'utilisez que name et price!
}

❌ Gaspillage de mémoire et bande passante!


Solution 1: Interface Projection​

Créer une interface:

public interface ProductSummary {
String getName();
Double getPrice();
}

Repository:

List<ProductSummary> findAllBy();

Utilisation:

List<ProductSummary> summaries = productRepo.findAllBy();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}

✅ Charge seulement name et price!


Solution 2: DTO avec @Query​

public class ProductDTO {
private String name;
private Double price;

public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
}

Repository:

@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findAllDTOs();

Stratégies de Fetch​

FetchType.LAZY (Recommandé)​

@ManyToOne(fetch = FetchType.LAZY)
private Department department;

Avantages:

  • ✅ Charge seulement si nécessaire
  • ✅ Économise la mémoire

Inconvénient:

  • ⚠️ Peut causer N+1

Solution: Utiliser JOIN FETCH quand nécessaire


FetchType.EAGER (À Éviter)​

@ManyToOne(fetch = FetchType.EAGER)
private Department department;

Avantages:

  • ✅ Données toujours disponibles

Inconvénients:

  • ❌ Charge même si non utilisé
  • ❌ Peut charger trop de données

Règle: Utilisez LAZY par défaut!


Pagination + Tri + Performance: Exemple Complet​

public interface ProductRepository extends JpaRepository<Product, Long> {

// Pagination + Tri + Projection
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.category = :category")
Page<ProductDTO> findByCategoryOptimized(
@Param("category") String category,
Pageable pageable
);

// Pagination + Tri + JOIN FETCH
@Query("SELECT p FROM Product p JOIN FETCH p.category")
Page<Product> findAllWithCategory(Pageable pageable);
}

Utilisation:

// Page 0, 20 éléments, triés par prix DESC
Pageable pageable = PageRequest.of(0, 20, Sort.by("price").descending());

// Chargement optimisé
Page<ProductDTO> page = productRepo.findByCategoryOptimized("Electronics", pageable);

System.out.println("Total: " + page.getTotalElements());
System.out.println("Pages: " + page.getTotalPages());

for (ProductDTO dto : page.getContent()) {
System.out.println(dto.getName() + ": " + dto.getPrice());
}

Conseils de Performance​

1. Toujours Paginer les Grandes Listes​

// ❌ MAUVAIS
List<Product> products = productRepo.findAll(); // Tous les produits!

// ✅ BON
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20));

2. Utiliser des Projections​

// ❌ Charge toute l'entité
List<Product> products = productRepo.findAll();

// ✅ Charge seulement les champs nécessaires
List<ProductSummary> summaries = productRepo.findAllBy();

3. Éviter le N+1​

// ❌ N+1 Problem
List<Employee> employees = empRepo.findAll();
employees.forEach(e -> System.out.println(e.getDepartment().getName()));

// ✅ JOIN FETCH
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();

4. Utiliser LAZY par Défaut​

// ✅ BON
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

// ❌ ÉVITER
@ManyToOne(fetch = FetchType.EAGER)
private Department department;

5. Limiter la Profondeur des Jointures​

// ❌ Trop de JOIN
@Query("SELECT e FROM Employee e " +
"JOIN FETCH e.department d " +
"JOIN FETCH d.company c " +
"JOIN FETCH c.industry i")

// ✅ Seulement ce qui est nécessaire
@Query("SELECT e FROM Employee e JOIN FETCH e.department")

Monitoring des Requêtes​

Activer les Logs SQL​

application.properties:

# Afficher les requêtes SQL
spring.jpa.show-sql=true

# Formater le SQL (lisible)
spring.jpa.properties.hibernate.format_sql=true

# Afficher les paramètres
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# Statistiques Hibernate
spring.jpa.properties.hibernate.generate_statistics=true

Console:

Hibernate: 
select
product0_.id as id1_0_,
product0_.name as name2_0_,
product0_.price as price3_0_
from
product product0_ limit ?

Résumé du Chapitre​

Pagination​

Pageable pageable = PageRequest.of(page, size, sort);
Page<Entity> page = repo.findAll(pageable);

Tri​

Sort sort = Sort.by("field").ascending();
Sort multi = Sort.by("field1").and(Sort.by("field2").descending());

Problème N+1​

// ❌ Problème
List<Entity> list = repo.findAll();
list.forEach(e -> e.getRelation());

// ✅ Solution
@Query("SELECT e FROM Entity e JOIN FETCH e.relation")
List<Entity> findAllWithRelation();

Projections​

// Interface
public interface Summary {
String getName();
}

// DTO
@Query("SELECT new DTO(e.name) FROM Entity e")
List<DTO> findDTOs();

Auto-Évaluation​

  1. Comment créer une page avec 20 éléments triés par prix?
  2. Qu'est-ce que le problème N+1?
  3. Comment le résoudre?
  4. Pourquoi utiliser des projections?

Réponses:

  1. PageRequest.of(0, 20, Sort.by("price").ascending())
  2. 1 requête principale + N requêtes pour les relations
  3. JOIN FETCH ou @EntityGraph
  4. Pour charger seulement les champs nécessaires

Félicitations! 🎉​

Vous avez terminé toute la théorie du Module 6!

Vous maîtrisez maintenant:

  • ✅ 15+ mots-clés de conventions
  • ✅ Les 3 types de relations
  • ✅ @Query (JPQL et SQL)
  • ✅ Pagination et performance

Prochaine étape: Mettre en pratique avec les TPs!


➡️ Commencer TP6.1: Conventions Avancées

⬅️ Chapitre précédent: @Query

⬅️ Retour au Module 6