977 palabras
5 minutos
Ordenamiento y Paginación en Spring Boot

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#

@Repository
public 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#

@Service
public 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

@SortDefault define 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:

Terminal window
# Ordenar por apellido (usa el valor por defecto)
GET /api/employees/all
# Ordenar por salario descendente
GET /api/employees/all?sort=salary,desc
# Ordenar por múltiples campos
GET /api/employees/all?sort=department,asc&sort=lastName,asc
# Sin especificar dirección (por defecto es ASC)
GET /api/employees/all?sort=firstName

Ordenamiento 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#

@Repository
public 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);
}
TIP

No necesitas implementar estos métodos. Spring Data JPA genera automáticamente las consultas SQL con LIMIT y OFFSET basándose en el objeto Pageable.

Paso 2: Servicio#

@Service
public 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

@PageableDefault define valores por defecto cuando el cliente no envía parámetros de paginación. La página comienza en 0 (primera página).

Uso desde el cliente#

El cliente puede combinar paginación y ordenamiento en las peticiones HTTP:

Terminal window
# Primera página con valores por defecto
GET /api/employees
# Segunda página (página 1) con 50 elementos
GET /api/employees?page=1&size=50
# Con ordenamiento
GET /api/employees?sort=salary,desc
# Múltiples criterios de ordenamiento
GET /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,desc

Ordenamiento 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:

@GetMapping
public 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:

@RestControllerAdvice
public 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:

Terminal window
GET /api/employees?sort=invalidField,desc

Respuesta de error:

{
"type": "about:blank",
"title": "Invalid Sort Field",
"status": 400,
"detail": "Invalid property 'invalidField' specified in request"
}
NOTE

ProblemDetail es 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.

Ordenamiento y Paginación en Spring Boot
https://blog.miikuru002.dev/posts/sorting-and-pagination-in-spring-boot/
Autor
J. Ortega
Publicado el
2026-01-21
Licencia
CC BY-NC-SA 4.0