Saltar al contenido principal

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​

  1. Sintaxis JPQL (Java Persistence Query Language)
  2. @Query con JPQL
  3. @Query con SQL nativo
  4. Parámetros nombrados con @Param
  5. 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​

AspectoJPQLSQL
ObjetivoEntidades JavaTablas de BD
SintaxisFROM Product pFROM product
Camposp.nameproduct.name
Relacionesp.category.nameJOIN 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:

  • @Modifying obligatorio
  • @Transactional obligatorio
  • 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​

  1. ¿Cuál es la diferencia entre JPQL y SQL?
  2. ¿Cómo pasar parámetros nombrados?
  3. ¿Qué hace JOIN FETCH?
  4. ¿Cuándo usar @Modifying?

Respuestas:

  1. JPQL usa las entidades Java, SQL las tablas de BD
  2. Con :nombreParam y @Param("nombreParam")
  3. Carga las relaciones en una sola consulta (evita N+1)
  4. Para UPDATE o DELETE

Próximo Paso​

¡Domina @Query! Pase a Paginación y Performance.

➡️ Capítulo 4: Paginación y Performance

⬅️ Capítulo anterior: Relaciones

⬅️ Volver al Módulo 6