Hoy nos vamos a meter de lleno en la creación de consultas personalizadas usando JPQL (Java Persistence Query Language). Esto nos permite escribir consultas más complejas que van más allá de los métodos por defecto de los repositorios. Los métodos genéricos del repositorio están muy bien, pero a veces necesitamos flexibilidad.
1. Conociendo JPQL
JPQL (Java Persistence Query Language) es el lenguaje de consultas que ofrece JPA, muy parecido a SQL, pero trabaja con objetos en vez de con tablas. Eso es clave: operamos no a nivel de la base de datos, sino sobre los objetos que están mapeados a entidades.
Ejemplo de una consulta SQL simple:
SELECT * FROM employees WHERE department = 'IT';
Ejemplo de una consulta JPQL equivalente:
SELECT e FROM Employee e WHERE e.department = 'IT'
Fíjate:
- Trabajamos con clases y sus campos (
Employeeydepartment), no con nombres de tablas y columnas (employeesydepartment). - JPQL es sensible a los nombres de la clase y sus campos. Un error en el nombre provocará un
QuerySyntaxException.
¿Por qué usar JPQL?
A veces los métodos estándar del repositorio, como findById, save o deleteAll, no son suficientes para obtener datos complejos. JPQL permite:
- Hacer agregaciones (por ejemplo,
AVG,SUM,COUNT). - Escribir condiciones más complejas (por ejemplo, joins y filtros).
- Tener un control más fino sobre la selección de datos, por ejemplo aplicando ordenaciones y límites.
2. Escribir consultas personalizadas
Vamos a crear un ejemplo para ver cómo escribir y usar JPQL. Imagina que tenemos un sistema de gestión de empleados, con una entidad Employee que tiene estos campos:
id(identificador único del empleado).name(nombre del empleado).department(departamento del empleado).salary(salario del empleado).
Paso 1: Crear la entidad
La clase Employee se ve así:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String department;
@Column(nullable = false)
private Double salary;
// Getters y setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
}
Paso 2: Crear el repositorio con una consulta personalizada
Añadimos la interfaz del repositorio EmployeeRepository. Heredará de JpaRepository para que podamos usar los métodos estándar. Además definiremos un método propio usando JPQL.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// Consulta personalizada para obtener empleados de un departamento dado
@Query("SELECT e FROM Employee e WHERE e.department = :department")
List<Employee> findByDepartment(@Param("department") String department);
// Consulta personalizada para obtener empleados con salario por encima de un valor
@Query("SELECT e FROM Employee e WHERE e.salary > :salary")
List<Employee> findEmployeesWithSalaryAbove(@Param("salary") Double salary);
// Consulta personalizada con agregación
@Query("SELECT AVG(e.salary) FROM Employee e WHERE e.department = :department")
Double findAverageSalaryByDepartment(@Param("department") String department);
}
Ten en cuenta:
- Se usa la anotación
@Querypara escribir la consulta JPQL personalizada. - Usamos parámetros (
:departmento:salary) para pasar valores a la consulta. - La anotación
@Paramenlaza los parámetros del método con los de la consulta.
Paso 3: Uso del repositorio
Vamos a llamar a nuestros métodos desde la capa de servicio o desde un controlador.
Ejemplo de servicio:
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
public List<Employee> getEmployeesByDepartment(String department) {
return employeeRepository.findByDepartment(department);
}
public List<Employee> getEmployeesWithHighSalary(Double salary) {
return employeeRepository.findEmployeesWithSalaryAbove(salary);
}
public Double getAverageSalary(String department) {
return employeeRepository.findAverageSalaryByDepartment(department);
}
}
Ejemplo de controlador:
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/employees")
public class EmployeeController {
private final EmployeeService employeeService;
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@GetMapping("/by-department")
public List<Employee> getEmployeesByDepartment(@RequestParam String department) {
return employeeService.getEmployeesByDepartment(department);
}
@GetMapping("/high-salary")
public List<Employee> getEmployeesWithHighSalary(@RequestParam Double salary) {
return employeeService.getEmployeesWithHighSalary(salary);
}
@GetMapping("/average-salary")
public Double getAverageSalary(@RequestParam String department) {
return employeeService.getAverageSalary(department);
}
}
Paso 4: Probar las consultas
Al hacer GET /employees/by-department?department=IT obtendrás todos los empleados del departamento IT.
Con GET /employees/high-salary?salary=50000 — todos los empleados con salario mayor a 50,000.
Y con GET /employees/average-salary?department=HR — la media de los salarios de los empleados del departamento HR.
3. Ejemplos más avanzados
Si te parece que todo esto es demasiado simple, vamos a meterle algo de complejidad.
Consulta con ordenación
Podemos añadir ordenación usando ORDER BY en JPQL:
@Query("SELECT e FROM Employee e ORDER BY e.salary DESC")
List<Employee> findAllEmployeesBySalaryDesc();
Consulta con LIKE
Podemos buscar empleados por parte del nombre:
@Query("SELECT e FROM Employee e WHERE e.name LIKE %:keyword%")
List<Employee> findByNameContaining(@Param("keyword") String keyword);
Consulta con join
Si tenemos una entidad Department relacionada con Employee, podemos usar JOIN:
@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :departmentName")
List<Employee> findByDepartmentName(@Param("departmentName") String departmentName);
4. Manejo de errores típicos
Los errores más comunes son dos: errores de sintaxis en la consulta y errores con los parámetros. Por ejemplo:
- Si en la consulta escribes
SELECT e FROM Employee e WHERE e.departments = :departmenty el campo en la clase se llamadepartment, Spring lanzará una excepciónIllegalArgumentExceptioncon la descripción del error. - Si olvidas pasar un parámetro en
@Param, Spring tampoco estará contento.
Consejo: prueba siempre las consultas en etapas tempranas para evitar sorpresas desagradables.
5. Cuándo usar JPQL
Usar JPQL está justificado en los siguientes casos:
- Necesitas una selección de datos compleja (por ejemplo, agregaciones o joins).
- Los métodos estándar del repositorio no son suficientes.
- Quieres escribir una consulta basada en la lógica de negocio sin meterte demasiado en SQL.
Si las consultas se vuelven demasiado complejas, quizá debas considerar usar consultas SQL nativas o incluso mover la lógica a procedimientos almacenados.
@Query(value = "SELECT * FROM employees WHERE salary > :salary", nativeQuery = true)
List<Employee> findHighSalaryEmployees(@Param("salary") Double salary);
Espero que ahora te sientas con más confianza, como si acabaras de derrotar definitivamente ese bug que te tenía frito durante tres semanas. ¡Es hora de construir consultas realmente potentes en tus aplicaciones!
GO TO FULL VERSION