Introducción
Imagina que tu aplicación intenta cargar 80,000 registros de una vez. El resultado: respuestas lentas, consumo excesivo de memoria y una aplicación que se vuelve inutilizable. Este problema es más común de lo que parece. La solución está en ordenar y paginar los datos. En lugar de cargar miles de registros, trabajamos con pequeños fragmentos ordenados que el usuario puede navegar fácilmente. En este artículo, aprenderemos cómo implementar ordenamiento y paginación en Spring Boot usando JPA.
Ordenamiento con Sort
Sort es una clase de Spring Data que permite definir criterios de ordenamiento para las consultas. Puede usarse de forma independiente o combinada con paginación. Veamos cómo implementarlo paso a paso cuando solo necesitamos ordenar sin paginar:
Implementación
Paso 1: Repositorio
@Repositorypublic interface IEmployeeRepository extends JpaRepository<Employee, Long> { // Spring Data JPA automáticamente soporta Sort List<Employee> findAll(Sort sort);
// También funciona con métodos personalizados List<Employee> findByDepartment(String department, Sort sort);}Paso 2: Servicio
@Servicepublic class EmployeeService { private final IEmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; }
public List<Employee> getAllEmployeesSorted(Sort sort) { return employeeRepository.findAll(sort); }}Paso 3: Controller con @SortDefault
@RestController@RequestMapping("/api/employees")public class EmployeeController { private final EmployeeService employeeService;
public EmployeeController(EmployeeService employeeService) { this.employeeService = employeeService; }
@GetMapping("/all") public List<Employee> getAllEmployeesSorted( @SortDefault(sort = "lastName", direction = Sort.Direction.ASC) Sort sort ) { return employeeService.getAllEmployeesSorted(sort); }}TIP
@SortDefaultdefine valores por defecto cuando el cliente no envía parámetros de ordenamiento.
Uso desde el cliente
El cliente puede especificar criterios de ordenamiento en las peticiones HTTP:
# Ordenar por apellido (usa el valor por defecto)GET /api/employees/all
# Ordenar por salario descendenteGET /api/employees/all?sort=salary,desc
# Ordenar por múltiples camposGET /api/employees/all?sort=department,asc&sort=lastName,asc
# Sin especificar dirección (por defecto es ASC)GET /api/employees/all?sort=firstNameOrdenamiento Programático
También podemos crear el ordenamiento directamente en el código:
@GetMapping("/top-earners")public List<Employee> getTopEarners() { // Crear Sort con ordenamiento personalizado Sort sort = Sort.by("salary").descending() .and(Sort.by("lastName").ascending());
return employeeService.getAllEmployeesSorted(sort);}Paginación y Ordenamiento
Pageable es la interfaz de Spring Data JPA que integra paginación y ordenamiento simultáneamente. En lugar de cargar todos los registros, dividimos los datos en páginas (por ejemplo, 50 registros por página) y los ordenamos según criterios específicos. Los resultados se devuelven en un objeto Page<T> que contiene tanto los datos solicitados como información útil sobre la paginación:
- content: Lista de elementos de la página actual
- totalElements: Total de elementos en la base de datos
- totalPages: Total de páginas disponibles
- number: Número de página actual (comienza en 0)
- size: Tamaño de la página
- first y last: Indicadores de primera y última página
Implementación
Paso 1: Repositorio
@Repositorypublic interface IEmployeeRepository extends JpaRepository<Employee, Long> { // Spring Data JPA automáticamente soporta Pageable Page<Employee> findAll(Pageable pageable);
// También funciona con métodos personalizados Page<Employee> findByDepartment(String department, Pageable pageable);}TIPNo necesitas implementar estos métodos. Spring Data JPA genera automáticamente las consultas SQL con
LIMITyOFFSETbasándose en el objetoPageable.
Paso 2: Servicio
@Servicepublic class EmployeeService { private final IEmployeeRepository employeeRepository;
public EmployeeService(IEmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; }
public Page<Employee> getAllEmployees(Pageable pageable) { return employeeRepository.findAll(pageable); }
public Page<Employee> getEmployeesByDepartment(String department, Pageable pageable) { return employeeRepository.findByDepartment(department, pageable); }}Paso 3: Controller con @PageableDefault
@RestController@RequestMapping("/api/employees")public class EmployeeController { private final EmployeeService employeeService;
public EmployeeController(EmployeeService employeeService) { this.employeeService = employeeService; }
@GetMapping public Page<Employee> getEmployees( @PageableDefault( page = 0, // Página por defecto size = 50, // Tamaño por defecto sort = "lastName", // Campo por defecto para ordenar direction = Sort.Direction.ASC // Dirección del ordenamiento ) Pageable pageable ) { return employeeService.getAllEmployees(pageable); }
@GetMapping("/department/{department}") public Page<Employee> getEmployeesByDepartment( @PathVariable String department, @PageableDefault(size = 20, sort = "salary", direction = Sort.Direction.DESC) Pageable pageable ) { return employeeService.getEmployeesByDepartment(department, pageable); }}TIP
@PageableDefaultdefine valores por defecto cuando el cliente no envía parámetros de paginación. La página comienza en0(primera página).
Uso desde el cliente
El cliente puede combinar paginación y ordenamiento en las peticiones HTTP:
# Primera página con valores por defectoGET /api/employees
# Segunda página (página 1) con 50 elementosGET /api/employees?page=1&size=50
# Con ordenamientoGET /api/employees?sort=salary,desc
# Múltiples criterios de ordenamientoGET /api/employees?sort=department,asc&sort=salary,desc
# Paginación y ordenamiento juntos (tercera página con 25 elementos)GET /api/employees?page=2&size=25&sort=salary,descOrdenamiento Programático con Paginación
Podemos crear objetos Pageable con ordenamiento personalizado directamente en el código:
@GetMapping("/top-earners")public Page<Employee> getTopEarners( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "25") int size) { // Crear Pageable con ordenamiento personalizado Pageable pageable = PageRequest.of( page, size, Sort.by("salary").descending() .and(Sort.by("lastName").ascending()) );
return employeeService.getAllEmployees(pageable);}Personalizando la Respuesta
Cuando usamos Page<T> como tipo de retorno, Spring Boot serializa la respuesta con muchos metadatos que pueden ser innecesarios. Por eso se recomienda crear un record de response personalizado con solo los campos que necesitamos:
public record PageResponse<T>( List<T> content, int page, int size, long totalElements, int totalPages, boolean first, boolean last) { public static <T> PageResponse<T> of(Page<T> page) { return new PageResponse<>( page.getContent(), page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), page.isFirst(), page.isLast() ); }}Ejemplo de uso en el controller:
@GetMappingpublic PageResponse<Employee> getEmployees( @PageableDefault(size = 50) Pageable pageable) { Page<Employee> page = employeeService.getAllEmployees(pageable); return PageResponse.of(page);}Esto genera una respuesta limpia y con solo los datos necesarios:
{ "content": [...], "page": 0, "size": 50, "totalElements": 80000, "totalPages": 1600, "first": true, "last": false}Manejo de Excepciones en el Ordenamiento
Cuando el cliente intenta ordenar por un campo que no existe en la entidad, Spring lanza una PropertyReferenceException. Podemos capturar esta excepción y devolver un mensaje de error claro usando @RestControllerAdvice:
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(PropertyReferenceException.class) public ProblemDetail handlePropertyReferenceException(PropertyReferenceException ex) { var problemDetail = ProblemDetail.forStatusAndDetail( HttpStatus.BAD_REQUEST, "Invalid property '" + ex.getPropertyName() + "' specified in request" ); problemDetail.setTitle("Invalid Sort Field"); return problemDetail; }}Ejemplo de petición que genera el error:
GET /api/employees?sort=invalidField,descRespuesta de error:
{ "type": "about:blank", "title": "Invalid Sort Field", "status": 400, "detail": "Invalid property 'invalidField' specified in request"}NOTE
ProblemDetailes una clase de Spring que implementa el estándar RFC 7807 para representar errores en APIs REST.
Conclusión
La paginación y el ordenamiento son técnicas fundamentales para construir aplicaciones Spring Boot escalables y con buen rendimiento. Con las herramientas que nos proporciona Spring Data JPA, implementar estas funcionalidades es sorprendentemente simple. Siguiendo estas prácticas, nuestras APIs podrán manejar millones de registros sin problemas, proporcionando respuestas rápidas y una excelente experiencia de usuario.