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
- Syntaxe JPQL (Java Persistence Query Language)
- @Query avec JPQL
- @Query avec SQL natif
- Paramètres nommés avec @Param
- 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
| Aspect | JPQL | SQL |
|---|---|---|
| Cible | Entités Java | Tables de BD |
| Syntaxe | FROM Product p | FROM product |
| Champs | p.name | product.name |
| Relations | p.category.name | JOIN 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:
@Modifyingobligatoire@Transactionalobligatoire- 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
- Quelle est la différence entre JPQL et SQL?
- Comment passer des paramètres nommés?
- Que fait
JOIN FETCH? - Quand utiliser
@Modifying?
Réponses:
- JPQL utilise les entités Java, SQL les tables BD
- Avec
:nomParamet@Param("nomParam") - Charge les relations en une seule requête (évite N+1)
- Pour UPDATE ou DELETE
Prochaine Étape
Vous maîtrisez @Query! Passez à la Pagination et Performance.
➡️ Chapitre 4: Pagination et Performance