Aller au contenu principal

Chapitre 3: Requêtes Personnalisées avec @Query

Introduction​

Les conventions de nommage sont puissantes, mais parfois insuffisantes pour des requêtes complexes.

Ce chapitre vous apprend à écrire vos propres requêtes avec @Query!

Ce que vous allez apprendre​

  1. Syntaxe JPQL (Java Persistence Query Language)
  2. @Query avec JPQL
  3. @Query avec SQL natif
  4. Paramètres nommés avec @Param
  5. Requêtes de modification (@Modifying)

Durée: 1h


Pourquoi @Query?​

Limites des Conventions​

Essayez de créer cette requête avec les conventions:

SELECT p FROM Product p 
WHERE p.price < :maxPrice
AND p.stock > 0
AND p.category.name IN ('Electronics', 'Computers')
ORDER BY p.price ASC

Nom de méthode résultant:

findByPriceLessThanAndStockGreaterThanAndCategoriesNameInOrderByPriceAsc(
Double maxPrice,
Integer minStock,
List<String> categories
)

❌ Trop long! Pas lisible!


Solution: @Query​

@Query("SELECT p FROM Product p " +
"WHERE p.price < :maxPrice " +
"AND p.stock > 0 " +
"AND p.category.name IN :categories " +
"ORDER BY p.price ASC")
List<Product> findAvailableProducts(
@Param("maxPrice") Double maxPrice,
@Param("categories") List<String> categories
);

✅ Clair! Lisible! Puissant!


JPQL vs SQL​

Différences Fondamentales​

AspectJPQLSQL
CibleEntités JavaTables de BD
SyntaxeFROM Product pFROM product
Champsp.nameproduct.name
Relationsp.category.nameJOIN nécessaire
Portable✅ Oui❌ Dépend de la BD

Exemple Comparatif​

JPQL (recommandé):

@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findCheap(@Param("max") Double max);

SQL Natif:

@Query(value = "SELECT * FROM product WHERE price < :max", 
nativeQuery = true)
List<Product> findCheap(@Param("max") Double max);

Préférez JPQL: Plus portable, plus orienté objet!


JPQL: Les Bases​

1. SELECT Simple​

@Query("SELECT p FROM Product p")
List<Product> findAllProducts();

// Équivalent SQL:
// SELECT * FROM product

Points clés:

  • Product = Nom de la classe (pas de la table)
  • p = Alias pour l'entité

2. WHERE Conditions​

@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
List<Product> findByPriceLessThan(@Param("maxPrice") Double maxPrice);

@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);

@Query("SELECT p FROM Product p WHERE p.active = true AND p.stock > 0")
List<Product> findAvailable();

3. ORDER BY​

@Query("SELECT p FROM Product p ORDER BY p.price ASC")
List<Product> findAllOrderByPrice();

@Query("SELECT p FROM Product p ORDER BY p.price DESC, p.name ASC")
List<Product> findAllSorted();

4. Jointures​

Sans JOIN explicite (utilise la relation):

@Query("SELECT e FROM Employee e WHERE e.department.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);

Avec JOIN:

@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);

Avec JOIN FETCH (évite N+1):

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

Paramètres: Positionnels vs Nommés​

Paramètres Positionnels (?1, ?2)​

@Query("SELECT p FROM Product p WHERE p.price BETWEEN ?1 AND ?2")
List<Product> findByPriceRange(Double min, Double max);

Problème: Ordre important, pas clair


Paramètres Nommés (:name) - Recommandé​

@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
List<Product> findByPriceRange(
@Param("min") Double minPrice,
@Param("max") Double maxPrice
);

✅ Avantages:

  • Plus clair
  • Ordre indépendant
  • Moins d'erreurs

Requêtes Complexes​

Exemple 1: Recherche Multi-Critères​

@Query("SELECT p FROM Product p " +
"WHERE (:name IS NULL OR p.name LIKE %:name%) " +
"AND (:minPrice IS NULL OR p.price >= :minPrice) " +
"AND (:maxPrice IS NULL OR p.price <= :maxPrice) " +
"AND (:category IS NULL OR p.category = :category)")
List<Product> searchProducts(
@Param("name") String name,
@Param("minPrice") Double minPrice,
@Param("maxPrice") Double maxPrice,
@Param("category") String category
);

Utilisation:

// Tous les paramètres
productRepo.searchProducts("Laptop", 500.0, 1500.0, "Electronics");

// Seulement le nom
productRepo.searchProducts("Laptop", null, null, null);

// Prix et catégorie
productRepo.searchProducts(null, 500.0, 1500.0, "Electronics");

Exemple 2: Agrégations​

// Compter
@Query("SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Long countByCategory(@Param("category") String category);

// Somme
@Query("SELECT SUM(p.price) FROM Product p WHERE p.category = :category")
Double sumPricesByCategory(@Param("category") String category);

// Moyenne
@Query("SELECT AVG(p.price) FROM Product p")
Double getAveragePrice();

// Min/Max
@Query("SELECT MIN(p.price), MAX(p.price) FROM Product p")
Object[] getPriceRange();

Exemple 3: GROUP BY et HAVING​

@Query("SELECT p.category, COUNT(p) FROM Product p " +
"GROUP BY p.category " +
"HAVING COUNT(p) > :minCount")
List<Object[]> getCategoriesWithMinProducts(@Param("minCount") Long minCount);

Résultat:

List<Object[]> results = productRepo.getCategoriesWithMinProducts(5L);
for (Object[] row : results) {
String category = (String) row[0];
Long count = (Long) row[1];
System.out.println(category + ": " + count + " produits");
}

Projections: Retourner des DTOs​

Problème​

Vous ne voulez pas toute l'entité, juste quelques champs:

// ❌ Charge TOUTE l'entité
@Query("SELECT p FROM Product p")
List<Product> findAll();

Solution 1: Object[]​

@Query("SELECT p.name, p.price FROM Product p")
List<Object[]> findNameAndPrice();

Utilisation:

List<Object[]> results = productRepo.findNameAndPrice();
for (Object[] row : results) {
String name = (String) row[0];
Double price = (Double) row[1];
}

❌ Pas type-safe!


Solution 2: Interface Projection​

Créer une interface:

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

Utiliser dans @Query:

@Query("SELECT p.name AS name, p.price AS price FROM Product p")
List<ProductSummary> findSummaries();

Utilisation:

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

✅ Type-safe!


Solution 3: DTO avec Constructeur​

Créer un DTO:

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

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

// Getters
}

Utiliser dans @Query:

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

✅ Le meilleur! Type-safe et performant!


SQL Natif avec nativeQuery = true​

Quand Utiliser SQL Natif?​

Utilisez SQL natif pour:

  • ✅ Fonctions spécifiques à la BD (PostgreSQL, MySQL, etc.)
  • ✅ Requêtes très complexes
  • ✅ Optimisations spécifiques

Évitez pour:

  • ❌ Requêtes simples (utilisez JPQL)
  • ❌ Portabilité entre BDs

Syntaxe SQL Natif​

@Query(value = "SELECT * FROM product WHERE price < :maxPrice", 
nativeQuery = true)
List<Product> findCheapNative(@Param("maxPrice") Double maxPrice);

Points clés:

  • nativeQuery = true
  • Noms de tables et colonnes de la BD (pas des entités Java)

Exemple: Fonction PostgreSQL​

@Query(value = "SELECT * FROM product " +
"WHERE LOWER(name) = LOWER(:name)",
nativeQuery = true)
List<Product> findByNameCaseInsensitive(@Param("name") String name);

Requêtes de Modification: @Modifying​

UPDATE​

@Modifying
@Transactional
@Query("UPDATE Product p SET p.price = :newPrice WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("newPrice") Double newPrice);

Points clés:

  • @Modifying obligatoire
  • @Transactional obligatoire
  • Retourne le nombre de lignes modifiées

DELETE​

@Modifying
@Transactional
@Query("DELETE FROM Product p WHERE p.active = false")
int deleteInactiveProducts();

Attention: Incohérence du Cache​

Problème:

Product p = productRepo.findById(1L);  // Price = 100
productRepo.updatePrice(1L, 200.0); // Met à jour en BD
// Mais p.getPrice() retourne toujours 100! (cache)

Solution: Vider le cache

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Product p SET p.price = :price WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("price") Double price);

Named Queries​

Définir dans l'Entité​

@Entity
@NamedQuery(
name = "Product.findExpensive",
query = "SELECT p FROM Product p WHERE p.price > :minPrice"
)
public class Product {
// ...
}

Utiliser dans le Repository​

public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring trouve automatiquement la NamedQuery!
List<Product> findExpensive(@Param("minPrice") Double minPrice);
}

Exemples Complets​

Repository Produits Complet​

public interface ProductRepository extends JpaRepository<Product, Long> {

// 1. Recherche simple
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);

// 2. Recherche avec jointure
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.price < :max")
List<Product> findCheapWithCategory(@Param("max") Double max);

// 3. Agrégation
@Query("SELECT p.category.name, COUNT(p), AVG(p.price) " +
"FROM Product p GROUP BY p.category.name")
List<Object[]> getStatsByCategory();

// 4. Projection DTO
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.active = true")
List<ProductDTO> findActiveDTOs();

// 5. SQL natif
@Query(value = "SELECT * FROM product WHERE stock < 10",
nativeQuery = true)
List<Product> findLowStock();

// 6. Modification
@Modifying
@Transactional
@Query("UPDATE Product p SET p.active = false WHERE p.stock = 0")
int deactivateOutOfStock();
}

Bonnes Pratiques​

1. Noms de Méthodes Clairs​

// ✅ BON
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findByPriceLessThan(@Param("max") Double max);

// ❌ MAUVAIS
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> get(@Param("max") Double max);

2. Paramètres Nommés​

// ✅ BON
@Query("... WHERE p.price BETWEEN :min AND :max")
List<Product> find(@Param("min") Double min, @Param("max") Double max);

// ❌ MAUVAIS
@Query("... WHERE p.price BETWEEN ?1 AND ?2")
List<Product> find(Double min, Double max);

3. JOIN FETCH pour Performance​

// ❌ N+1 Problem
@Query("SELECT e FROM Employee e")
List<Employee> findAll();
// Puis e.getDepartment() fait une requête par employé!

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

4. JPQL vs SQL Natif​

// ✅ Préférez JPQL (portable)
@Query("SELECT p FROM Product p WHERE p.price < :max")

// ⚠️ SQL natif seulement si nécessaire
@Query(value = "SELECT * FROM product WHERE price < :max", nativeQuery = true)

Résumé du Chapitre​

@Query Permet​

✅ Requêtes complexes plus lisibles
✅ JPQL (orienté objet, portable)
✅ SQL natif (fonctions spécifiques BD)
✅ Projections (DTOs, interfaces)
✅ Agrégations (COUNT, SUM, AVG)
✅ Modifications (@Modifying)


Syntaxe JPQL​

@Query("SELECT e FROM Entity e " +
"WHERE e.field = :param " +
"ORDER BY e.field ASC")
List<Entity> findMethod(@Param("param") Type param);

SQL Natif​

@Query(value = "SELECT * FROM table WHERE column = :param", 
nativeQuery = true)
List<Entity> findMethod(@Param("param") Type param);

Auto-Évaluation​

  1. Quelle est la différence entre JPQL et SQL?
  2. Comment passer des paramètres nommés?
  3. Que fait JOIN FETCH?
  4. Quand utiliser @Modifying?

Réponses:

  1. JPQL utilise les entités Java, SQL les tables BD
  2. Avec :nomParam et @Param("nomParam")
  3. Charge les relations en une seule requête (évite N+1)
  4. Pour UPDATE ou DELETE

Prochaine Étape​

Vous maîtrisez @Query! Passez à la Pagination et Performance.

➡️ Chapitre 4: Pagination et Performance

⬅️ Chapitre précédent: Relations

⬅️ Retour au Module 6