
Requêtes personnalisées avec `@Query` (JPQL et SQL natif)
Apprenez à utiliser l'annotation @Query dans Spring Data JPA pour écrire des requêtes personnalisées complexes en JPQL ou en SQL natif, allant au-delà des méthodes de requête dérivées.
Quand les méthodes dérivées ne suffisent plus
Spring Data JPA offre un mécanisme puissant et pratique de méthodes de requête dérivées (Query Methods), où le nom de la méthode dans votre interface Repository est automatiquement traduit en une requête (JPQL ou SQL selon le contexte). Par exemple, findByEmail(String email) génère une requête pour trouver une entité par son champ `email`. C'est idéal pour les requêtes CRUD simples et les recherches basées sur les propriétés des entités.
Cependant, il arrive souvent que les besoins en matière de requêtes deviennent plus complexes :
- Requêtes impliquant des jointures complexes non triviales à exprimer via le nom de la méthode.
- Utilisation de fonctions spécifiques à la base de données (non standard SQL/JPQL).
- Besoin d'optimiser finement une requête pour des raisons de performance.
- Exécution de requêtes de mise à jour (UPDATE) ou de suppression (DELETE) en masse.
- Projection des résultats dans des DTOs spécifiques ou sélection de champs particuliers de manière complexe.
Pour ces scénarios, Spring Data JPA fournit l'annotation @Query. Placée sur une méthode d'interface Repository, elle vous permet de définir explicitement la requête à exécuter, soit en JPQL (Java Persistence Query Language), soit en SQL natif.
Requêtes personnalisées avec JPQL
JPQL est le langage de requête standard défini par la spécification JPA. Il ressemble beaucoup à SQL, mais il opère sur les objets entités et leurs attributs persistants (les champs de vos classes Java annotées @Entity), et non directement sur les tables et les colonnes de la base de données. C'est l'approche privilégiée lorsque vous utilisez @Query car elle reste indépendante de la base de données sous-jacente (dans la mesure du possible) et bénéficie des optimisations de l'ORM.
Syntaxe de base : La valeur de l'annotation @Query contient la chaîne de caractères de la requête JPQL.
Paramètres positionnels : Vous pouvez utiliser des paramètres indexés, préfixés par ? suivi d'un numéro (commençant à 1) correspondant à la position du paramètre dans la signature de la méthode.
public interface UserRepository extends JpaRepository {
// Trouver des utilisateurs actifs par email et nom (paramètres positionnels)
@Query("SELECT u FROM User u WHERE u.email = ?1 AND u.lastName = ?2 AND u.active = true")
List findActiveByEmailAndLastNamePositional(String email, String lastName);
}
Paramètres nommés (recommandé) : Une approche plus lisible et moins sujette aux erreurs consiste à utiliser des paramètres nommés, préfixés par : (ex: :emailParam). Vous liez ensuite les paramètres de la méthode aux noms dans la requête en utilisant l'annotation @Param sur chaque paramètre de la méthode.
import org.springframework.data.repository.query.Param;
// ... dans l'interface UserRepository ...
// Trouver des utilisateurs par email ou nom d'utilisateur (paramètres nommés)
@Query("SELECT u FROM User u WHERE u.email = :email OR u.username = :username")
Optional findByEmailOrUsernameNamed(@Param("email") String email, @Param("username") String username);
// Utilisation d'un IN avec une liste
@Query("SELECT u FROM User u WHERE u.status IN :statuses")
List findByStatusIn(@Param("statuses") List statuses);
// Exemple avec une jointure explicite (bien que souvent non nécessaire en JPQL)
// Supposons une relation ManyToOne vers une entité Address
@Query("SELECT u FROM User u JOIN u.address a WHERE a.city = :city")
List findUsersByCity(@Param("city") String city);
}
JPQL supporte les clauses SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, ainsi que les jointures (implicites ou explicites via JOIN, LEFT JOIN, JOIN FETCH pour le Eager Loading). Pour les requêtes de modification (UPDATE/DELETE), voir la section sur @Modifying.
Requêtes personnalisées avec SQL Natif
Parfois, JPQL ne suffit pas ou vous avez besoin d'exploiter une fonctionnalité spécifique à votre base de données (une fonction particulière, une syntaxe optimisée, des requêtes récursives non standard, etc.). Dans ces cas, vous pouvez écrire directement du SQL natif.
Pour cela, utilisez l'annotation @Query comme précédemment, mais ajoutez l'attribut nativeQuery = true. La chaîne de caractères de la requête doit alors être du SQL valide pour votre base de données cible (PostgreSQL, MySQL, Oracle, etc.).
Syntaxe et paramètres : La gestion des paramètres (positionnels avec ?1, ?2 ou nommés avec :paramName et @Param) fonctionne de la même manière qu'avec JPQL.
public interface ProductRepository extends JpaRepository {
// Trouver des produits dont le nom correspond à un pattern (SQL natif, syntaxe LIKE spécifique)
@Query(value = "SELECT * FROM products p WHERE p.name LIKE CONCAT('%', ?1, '%') AND p.is_active = true", nativeQuery = true)
List findActiveByNamePatternNative(String pattern);
// Exemple avec une fonction spécifique à PostgreSQL (ex: date_trunc)
@Query(value = "SELECT * FROM users u WHERE date_trunc('day', u.creation_timestamp) = :creationDate", nativeQuery = true)
List findUsersCreatedOnDate(@Param("creationDate") java.sql.Date creationDate);
}
Points importants avec SQL natif :
- Dépendance à la BDD : Votre requête est maintenant liée à la syntaxe spécifique de votre base de données. Si vous changez de base de données, vous devrez peut-être réécrire ces requêtes.
- Mapping des résultats : Hibernate/JPA fait de son mieux pour mapper les colonnes du résultat SQL aux champs de votre entité (basé sur les noms ou les annotations
@Column). Cependant, pour des projections complexes ou des noms de colonnes ambigus, vous pourriez avoir besoin d'utiliser des alias dans votre SQL (SELECT p.id as productId, p.name as productName ...) ou des mécanismes plus avancés comme@SqlResultSetMapping. - Noms des tables/colonnes : Vous devez utiliser les noms réels des tables et des colonnes de votre base de données, et non les noms des entités et des champs Java.
Requêtes de modification : @Modifying
L'annotation @Query peut également être utilisée pour exécuter des requêtes UPDATE ou DELETE (en JPQL ou SQL natif). Cependant, comme ces requêtes modifient l'état de la base de données, vous devez impérativement ajouter l'annotation @Modifying en plus de @Query.
L'annotation @Modifying signale à Spring Data JPA qu'il s'agit d'une requête de modification et non d'une simple sélection. Cela a plusieurs implications :
- Type de retour : La méthode annotée doit généralement retourner
voidouint(indiquant le nombre de lignes affectées par la requête). - Gestion transactionnelle : Les méthodes de modification doivent être exécutées dans une transaction. Assurez-vous que la méthode du service qui appelle cette méthode du repository est annotée avec
@Transactional(ou que la méthode du repository l'est elle-même, bien que ce soit moins courant pour les écritures). - Contexte de persistance : Les requêtes UPDATE/DELETE via
@Queryopèrent directement sur la base de données et peuvent rendre le contexte de persistance Hibernate (le cache de premier niveau) incohérent avec l'état réel de la BDD pour les entités affectées. Par défaut, le contexte n'est pas automatiquement vidé après une requête@Modifying. Vous pouvez ajouter@Modifying(clearAutomatically = true)pour forcer le détachement de toutes les entités gérées après l'exécution de la requête, ou gérer cela manuellement dans votre service.
Exemples :
import org.springframework.transaction.annotation.Transactional; // Nécessaire pour l'appelant
// ... dans une interface Repository ...
// Mettre à jour le statut d'un utilisateur (JPQL)
@Modifying
@Query("UPDATE User u SET u.status = :newStatus WHERE u.id = :userId")
int updateUserStatus(@Param("userId") Long userId, @Param("newStatus") UserStatus newStatus);
// Supprimer les utilisateurs inactifs avant une certaine date (JPQL)
@Modifying(clearAutomatically = true) // Pour vider le contexte après
@Query("DELETE FROM User u WHERE u.active = false AND u.lastLogin < :cutoffDate")
int deleteInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
// Désactiver des produits (SQL Natif)
@Modifying
@Query(value = "UPDATE products SET is_active = false WHERE category_id = :categoryId", nativeQuery = true)
int deactivateProductsByCategory(@Param("categoryId") Long categoryId);
}
Projections personnalisées avec @Query
@Query est également très utile pour retourner des résultats qui ne sont pas des entités complètes, mais plutôt des projections : soit des sous-ensembles de champs, soit des DTOs construits directement par la requête.
JPQL avec Constructor Expressions : JPQL permet de spécifier un constructeur de classe (souvent un DTO) dans la clause SELECT.
public interface ProductRepository extends JpaRepository {
// Retourner une liste de DTOs (ProductSummaryDto) avec seulement l'ID et le nom
@Query("SELECT new com.certiquizz.dto.ProductSummaryDto(p.id, p.name) FROM Product p WHERE p.category = :category")
List findProductSummariesByCategory(@Param("category") String category);
}
// Le DTO correspondant doit avoir un constructeur public acceptant les types retournés
// package com.certiquizz.dto;
// public class ProductSummaryDto {
// private Long id; private String name;
// public ProductSummaryDto(Long id, String name) { this.id = id; this.name = name; }
// // Getters...
// }
Pour les projections avec SQL natif, le mapping peut être plus complexe et nécessite souvent l'utilisation d'alias ou de @SqlResultSetMapping pour les cas avancés.
Conclusion : La flexibilité ultime pour vos requêtes
L'annotation @Query de Spring Data JPA est un outil indispensable lorsque les méthodes de requête dérivées atteignent leurs limites. Elle vous offre la flexibilité d'écrire des requêtes personnalisées, que ce soit en utilisant le langage orienté objet JPQL (recommandé pour la portabilité et l'intégration ORM) ou en descendant au niveau du SQL natif pour exploiter des fonctionnalités spécifiques à la base de données ou optimiser les performances. Combinée à @Param pour les paramètres nommés et à @Modifying pour les opérations de mise à jour ou de suppression, @Query vous permet de répondre à pratiquement tous les besoins en matière d'interrogation et de manipulation de données dans vos applications Spring Boot.