CodeGym /Cursos /Módulo 5. Spring /Uso de JPQL y consultas personalizadas en repositorios

Uso de JPQL y consultas personalizadas en repositorios

Módulo 5. Spring
Nivel 5 , Lección 8
Disponible

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 (Employee y department), no con nombres de tablas y columnas (employees y department).
  • 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:

  1. Se usa la anotación @Query para escribir la consulta JPQL personalizada.
  2. Usamos parámetros (:department o :salary) para pasar valores a la consulta.
  3. La anotación @Param enlaza 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:

  1. Si en la consulta escribes SELECT e FROM Employee e WHERE e.departments = :department y el campo en la clase se llama department, Spring lanzará una excepción IllegalArgumentException con la descripción del error.
  2. 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!

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION