Capítulo 3: Consultas Personalizadas con @Query
Introducción
Las convenciones de nomenclatura son poderosas, pero a veces insuficientes para consultas complejas.
¡Este capítulo le enseña a escribir sus propias consultas con @Query!
Lo que va a aprender
- Sintaxis JPQL (Java Persistence Query Language)
- @Query con JPQL
- @Query con SQL nativo
- Parámetros nombrados con @Param
- Consultas de modificación (@Modifying)
Duración: 1h
¿Por qué @Query?
Límites de las Convenciones
Intente crear esta consulta con las convenciones:
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
Nombre de método resultante:
findByPriceLessThanAndStockGreaterThanAndCategoriesNameInOrderByPriceAsc(
Double maxPrice,
Integer minStock,
List<String> categories
)
❌ ¡Demasiado largo! ¡No es legible!
Solución: @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
);
✅ ¡Claro! ¡Legible! ¡Potente!
JPQL vs SQL
Diferencias Fundamentales
| Aspecto | JPQL | SQL |
|---|---|---|
| Objetivo | Entidades Java | Tablas de BD |
| Sintaxis | FROM Product p | FROM product |
| Campos | p.name | product.name |
| Relaciones | p.category.name | JOIN necesario |
| Portable | ✅ Sí | ❌ Depende de la BD |
Ejemplo Comparativo
JPQL (recomendado):
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findCheap(@Param("max") Double max);
SQL Nativo:
@Query(value = "SELECT * FROM product WHERE price < :max",
nativeQuery = true)
List<Product> findCheap(@Param("max") Double max);
Prefiera JPQL: ¡Más portable, más orientado a objetos!
JPQL: Lo Básico
1. SELECT Simple
@Query("SELECT p FROM Product p")
List<Product> findAllProducts();
// SQL equivalente:
// SELECT * FROM product
Puntos clave:
Product= Nombre de la clase (no de la tabla)p= Alias para la entidad
2. Condiciones WHERE
@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. Uniones
Sin JOIN explícito (usa la relación):
@Query("SELECT e FROM Employee e WHERE e.department.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);
Con JOIN:
@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName")
List<Employee> findByDepartmentName(@Param("deptName") String deptName);
Con JOIN FETCH (evita N+1):
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();
Parámetros: Posicionales vs Nombrados
Parámetros Posicionales (?1, ?2)
@Query("SELECT p FROM Product p WHERE p.price BETWEEN ?1 AND ?2")
List<Product> findByPriceRange(Double min, Double max);
Problema: El orden es importante, no es claro
Parámetros Nombrados (:name) - Recomendado
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max")
List<Product> findByPriceRange(
@Param("min") Double minPrice,
@Param("max") Double maxPrice
);
✅ Ventajas:
- Más claro
- Orden independiente
- Menos errores
Consultas Complejas
Ejemplo 1: Búsqueda Multi-Criterio
@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
);
Uso:
// Todos los parámetros
productRepo.searchProducts("Laptop", 500.0, 1500.0, "Electronics");
// Solo el nombre
productRepo.searchProducts("Laptop", null, null, null);
// Precio y categoría
productRepo.searchProducts(null, 500.0, 1500.0, "Electronics");
Ejemplo 2: Agregaciones
// Contar
@Query("SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Long countByCategory(@Param("category") String category);
// Suma
@Query("SELECT SUM(p.price) FROM Product p WHERE p.category = :category")
Double sumPricesByCategory(@Param("category") String category);
// Promedio
@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();
Ejemplo 3: GROUP BY y 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);
Resultado:
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 + " productos");
}
Proyecciones: Retornar DTOs
Problema
No quiere toda la entidad, solo algunos campos:
// ❌ Carga TODA la entidad
@Query("SELECT p FROM Product p")
List<Product> findAll();
Solución 1: Object[]
@Query("SELECT p.name, p.price FROM Product p")
List<Object[]> findNameAndPrice();
Uso:
List<Object[]> results = productRepo.findNameAndPrice();
for (Object[] row : results) {
String name = (String) row[0];
Double price = (Double) row[1];
}
❌ ¡No es type-safe!
Solución 2: Proyección por Interfaz
Crear una interfaz:
public interface ProductSummary {
String getName();
Double getPrice();
}
Usar en @Query:
@Query("SELECT p.name AS name, p.price AS price FROM Product p")
List<ProductSummary> findSummaries();
Uso:
List<ProductSummary> summaries = productRepo.findSummaries();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}
✅ ¡Type-safe!
Solución 3: DTO con Constructor
Crear un DTO:
public class ProductDTO {
private String name;
private Double price;
public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
// Getters
}
Usar en @Query:
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findDTOs();
✅ ¡La mejor! ¡Type-safe y eficiente!
SQL Nativo con nativeQuery = true
¿Cuándo Usar SQL Nativo?
Use SQL nativo para:
- ✅ Funciones específicas de la BD (PostgreSQL, MySQL, etc.)
- ✅ Consultas muy complejas
- ✅ Optimizaciones específicas
Evite para:
- ❌ Consultas simples (use JPQL)
- ❌ Portabilidad entre BDs
Sintaxis SQL Nativo
@Query(value = "SELECT * FROM product WHERE price < :maxPrice",
nativeQuery = true)
List<Product> findCheapNative(@Param("maxPrice") Double maxPrice);
Puntos clave:
nativeQuery = true- Nombres de tablas y columnas de la BD (no de las entidades Java)
Ejemplo: Función PostgreSQL
@Query(value = "SELECT * FROM product " +
"WHERE LOWER(name) = LOWER(:name)",
nativeQuery = true)
List<Product> findByNameCaseInsensitive(@Param("name") String name);
Consultas de Modificación: @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);
Puntos clave:
@Modifyingobligatorio@Transactionalobligatorio- Retorna el número de filas modificadas
DELETE
@Modifying
@Transactional
@Query("DELETE FROM Product p WHERE p.active = false")
int deleteInactiveProducts();
Atención: Incoherencia de Caché
Problema:
Product p = productRepo.findById(1L); // Price = 100
productRepo.updatePrice(1L, 200.0); // Actualiza en BD
// Pero p.getPrice() aún retorna 100! (caché)
Solución: Vaciar la caché
@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
Definir en la Entidad
@Entity
@NamedQuery(
name = "Product.findExpensive",
query = "SELECT p FROM Product p WHERE p.price > :minPrice"
)
public class Product {
// ...
}
Usar en el Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// ¡Spring encuentra automáticamente la NamedQuery!
List<Product> findExpensive(@Param("minPrice") Double minPrice);
}
Ejemplos Completos
Repository de Productos Completo
public interface ProductRepository extends JpaRepository<Product, Long> {
// 1. Búsqueda simple
@Query("SELECT p FROM Product p WHERE p.name LIKE %:keyword%")
List<Product> searchByName(@Param("keyword") String keyword);
// 2. Búsqueda con unión
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.price < :max")
List<Product> findCheapWithCategory(@Param("max") Double max);
// 3. Agregación
@Query("SELECT p.category.name, COUNT(p), AVG(p.price) " +
"FROM Product p GROUP BY p.category.name")
List<Object[]> getStatsByCategory();
// 4. Proyección DTO
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.active = true")
List<ProductDTO> findActiveDTOs();
// 5. SQL nativo
@Query(value = "SELECT * FROM product WHERE stock < 10",
nativeQuery = true)
List<Product> findLowStock();
// 6. Modificación
@Modifying
@Transactional
@Query("UPDATE Product p SET p.active = false WHERE p.stock = 0")
int deactivateOutOfStock();
}
Buenas Prácticas
1. Nombres de Métodos Claros
// ✅ BUENO
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> findByPriceLessThan(@Param("max") Double max);
// ❌ MALO
@Query("SELECT p FROM Product p WHERE p.price < :max")
List<Product> get(@Param("max") Double max);
2. Parámetros Nombrados
// ✅ BUENO
@Query("... WHERE p.price BETWEEN :min AND :max")
List<Product> find(@Param("min") Double min, @Param("max") Double max);
// ❌ MALO
@Query("... WHERE p.price BETWEEN ?1 AND ?2")
List<Product> find(Double min, Double max);
3. JOIN FETCH para Performance
// ❌ Problema N+1
@Query("SELECT e FROM Employee e")
List<Employee> findAll();
// Luego e.getDepartment() hace una consulta por empleado!
// ✅ 1 sola consulta
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();
4. JPQL vs SQL Nativo
// ✅ Prefiera JPQL (portable)
@Query("SELECT p FROM Product p WHERE p.price < :max")
// ⚠️ SQL nativo solo si es necesario
@Query(value = "SELECT * FROM product WHERE price < :max", nativeQuery = true)
Resumen del Capítulo
@Query Permite
✅ Consultas complejas más legibles
✅ JPQL (orientado a objetos, portable)
✅ SQL nativo (funciones específicas de BD)
✅ Proyecciones (DTOs, interfaces)
✅ Agregaciones (COUNT, SUM, AVG)
✅ Modificaciones (@Modifying)
Sintaxis JPQL
@Query("SELECT e FROM Entity e " +
"WHERE e.field = :param " +
"ORDER BY e.field ASC")
List<Entity> findMethod(@Param("param") Type param);
SQL Nativo
@Query(value = "SELECT * FROM table WHERE column = :param",
nativeQuery = true)
List<Entity> findMethod(@Param("param") Type param);
Autoevaluación
- ¿Cuál es la diferencia entre JPQL y SQL?
- ¿Cómo pasar parámetros nombrados?
- ¿Qué hace
JOIN FETCH? - ¿Cuándo usar
@Modifying?
Respuestas:
- JPQL usa las entidades Java, SQL las tablas de BD
- Con
:nombreParamy@Param("nombreParam") - Carga las relaciones en una sola consulta (evita N+1)
- Para UPDATE o DELETE
Próximo Paso
¡Domina @Query! Pase a Paginación y Performance.
➡️ Capítulo 4: Paginación y Performance