La ordenación de los resultados de las consultas parece un asunto sencillo y con poco recorrido. Nada más alejado de la realidad; hay bastante de qué hablar, tal y como descubrirás en este capítulo.
Índice
- El problema con la ordenación
- Ordenación con Sort
- La clase Sort.Order
- ¿Qué hacemos con los nulos?
- Ordenando con funciones
- Sort tipado con TypedSort<T>
- Prioridad de ordenaciones
- Un detalle curioso
- Resumen
- Código de ejemplo
El problema con la ordenación
En el curso ya han aparecido ejemplos de ordenación de resultados. Recordemos un par de ellos.
- Consulta derivada con expresiones
OrderBy
al final:
List<Country> findTop3ByOrderByPopulationAsc();
- Consulta JPQL con cláusula
ORDER BY
:
@Query("""
SELECT new com.danielme.springdatajpa.model.dto.ConfederationSummaryRecord(c.id, c.name, COUNT(ct.id))
FROM Country ct INNER JOIN ct.confederation c
GROUP BY ct.confederation.id, c.name
ORDER BY c.name ASC""")
List<ConfederationSummaryRecord> getSummaryCountryCountAsRecord();
En las consultas anteriores hay un pequeño detalle en el que tal vez no hayas reparado: la ordenación es estática. Se establece en la definición de la consulta y no se puede cambiar.
Si necesitas aplicar distintos criterios de ordenación a una consulta, resulta tentador duplicarla y cambiar la ordenación. No lo hagas. Spring Data te permite declarar criterios dinámicos, esto es, en tiempo de ejecución. Puedes disfrutar de este superpoder tanto en las consultas derivadas como en las escritas con JPQL, además de en otros tipos que veremos en futuros capítulos.
Ordenación con Sort
Aquí tienes un primer ejemplo de ordenación dinámica:
List<Country> findTop3By(Sort sort);
Es lo que parece: se añade a la signatura del método un parámetro de tipo Sort
con los criterios de ordenación. No es la primera vez que vemos un método así en el curso, aunque su presentación pasó desapercibida. El siguiente diagrama pertenece al capítulo tres e ilustra los repositorios genéricos. En esta ocasión los métodos con un parámetro Sort
aparecen subrayados en rojo.

Recuerda que no estás obligado a heredar de estos repositorios si precisas de algunos de sus métodos; copia los que necesites en tus repositorios.
Esta imagen es el «mapa» de Sort
(clic para ampliar) en Spring Data Commons 3.0.0.
El diagrama parece intrincado, pero no te asustes. La clave es que Sort
contiene una lista de objetos de su clase interna, estática e inmutable Sort.Order
. Cada order
de esa lista representa un criterio de ordenación para un atributo de la entidad —se admite la navegación por las relaciones con el operador "."
— o un alias declarado en la proyección. El criterio incluye el sentido (enumerado Sort.Direction
), el tratamiento de los nulos (enumerado Sort.NullHandling
) y el de la capitalización (el flag llamado ignoreCase
). No me olvido de la clase TypedSort
; la examinaremos más adelante.
IMPORTANTE. El tratamiento de los nulos definido por Spring Data y presentado en este capítulo no está implementado en Spring Data JPA. Lo recoge una incidencia registrada en GitHub.
Esta es la declaración de los elementos anteriores:
public class Sort implements Streamable<org.springframework.data.domain.Sort.Order>, Serializable {
private static final long serialVersionUID = 5737186511678863905L;
private static final Sort UNSORTED = Sort.by(new Order[0]);
public static final Direction DEFAULT_DIRECTION = Direction.ASC;
private final List<Order> orders;
//...
public static class Order implements Serializable {
private static final boolean DEFAULT_IGNORE_CASE = false;
private static final NullHandling DEFAULT_NULL_HANDLING = NullHandling.NATIVE;
private final Direction direction;
private final String property;
private final boolean ignoreCase;
private final NullHandling nullHandling;
//...
Volveremos sobre la clase Order
en la próxima sección.
¿Cómo se crean los objetos de la clase Sort
? Olvídate de sus constructores, pues son privados. En su lugar, recurre a las sobrecargas del método estático by
. Estas son las de uso más habitual:
public static Sort by(String... properties)
. Recibe unvarargs
con los nombres de los campos. El sentido de la ordenación es ascendente. Esto implica que la siguiente llamada equivale a invocar al métodofindTop3ByOrderByPopulationAsc
:
Sort sortByPopulation = Sort.by("population");
List<Country> lessPopulatedCountries = countryRepository.findTop3By(sortByPopulation);
public static Sort by(Direction direction, String... properties).
De nuevo, un método que recibe los campos en unvarargs
, pero acompañado del sentido. Esta llamada equivale a la anterior en sentido inverso:
Sort sortByPopulation = Sort.by(Sort.Direction.DESC, "population");
List<Country> mostPopulatedCountries = countryRepository.findTop3By(sortByPopulation);
Los métodos by
crean los objetos Order
idóneos. Y, por supuesto, la lista que los contiene respeta el orden de declaración de los campos, de tal modo que aparecerán en ese orden en la cláusula ORDER BY
.
Veamos un ejemplo con varios criterios de ordenación. Partamos de esta consulta:
@Query("""
SELECT new com.danielme.springdatajpa.model.dto.ConfederationSummaryRecord(
c.id, c.name, COUNT(ct.id) as countries)
FROM Country ct INNER JOIN ct.confederation c
GROUP BY ct.confederation.id, c.name""")
List<ConfederationSummaryRecord> getSummaryCountryCount(Sort sort);
Es una consulta JPQL con una proyección personalizada adaptada del capítulo precedente. Observa que he añadido alias a la proyección para nombrar sus componentes.
El caso que planteo: «ordenar primero por el número de países en sentido descendente y después por el nombre de la confederación en sentido ascendente». Con los métodos by
es imposible escribir tal ordenación porque cada propiedad tiene un sentido distinto. Aquí entran en juego estos métodos de Sort
:
Sort descending() | Devuelve un nuevo Sort con los mismos criterios de ordenación que el objeto sort sobre el que se invoca el método, pero todos en sentido descendente. |
Sort ascending() | Devuelve un nuevo Sort con los mismos criterios de ordenación que el objeto sort sobre el que se invoca el método, pero todos en sentido ascendente. |
Sort and(Sort sort) | Devuelve un nuevo Sort con los mismos criterios de ordenación que el objeto sort sobre el que se invoca el método más los del argumento. |
El hecho de que descending
, ascending
y and
devuelvan nuevos objetos Sort
es consecuencia de que los objetos Sort.Order
son inmutables, tal como señalé párrafos atrás. Asimismo, el retorno del objeto creado permite encadenar llamadas de manera fluida.
Ahora cuentas con las herramientas para declarar la ordenación que propuse. Una posible solución:
Sort sort = Sort.by("countries")
.descending()
.and(Sort.by("name"));
El primer campo a ordenar es el conteo de los países de cada confederación futbolística. Si bien puede indicarse el sentido como un argumento de by
, quiero mostrar cómo hacerlo con el método descending
.
El segundo campo se agrega llamando a and
. Recibe un Sort
, construido con by
, con el criterio de ordenación ascendente para name
. Su retorno es un nuevo objeto Sort
con toda la configuración realizada hasta el momento al que puedes añadir más criterios invocando and
.
El mismo ejemplo sin descending
:
Sort sort = Sort.by(Sort.Direction.DESC, "countries")
.and(Sort.by("name"));
¿Y si no quisieras establecer la ordenación? Los argumentos de tipo Sort
son opcionales, así que puedes proporcionar null
. Sin embargo, es más elegante crear una ordenación «vacía» con Sort#unsorted()
:
confederationRepository.getSummaryCountryCount(Sort.unsorted());
La clase Sort.Order
Los métodos by
crean un Order
para cada campo de ordenación, asignando a ignoreCase
y nullHandling
las constantes DEFAULT_IGNORE_CASE
(es false
) y DEFAULT_NULL_HANDLING
. Esto significa que, en general, solo crearás objetos Order
cuando necesites especificar los valores ignoreCase
y \ o nullHandling
. Reitero que este último valor carece de efecto alguno en Spring Data JPA.
Puedes crear objetos Order
de dos formas:
- Con los constructores públicos:
public Order(@Nullable Direction direction, String property) {
this(direction, property, DEFAULT_IGNORE_CASE, DEFAULT_NULL_HANDLING);
}
public Order(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
this(direction, property, DEFAULT_IGNORE_CASE, nullHandlingHint);
}
- Con métodos estáticos:
public static Order asc(String property) {
return new Order(Direction.ASC, property, DEFAULT_NULL_HANDLING);
}
public static Order desc(String property) {
return new Order(Direction.DESC, property, DEFAULT_NULL_HANDLING);
}
public static Order by(String property) {
return new Order(DEFAULT_DIRECTION, property);
}
Una vez tengas tus objetos Order
necesitarás un Sort
que los contenga. Cómo cabría esperar, Sort
provee métodos by
que reciben objetos Order
en una lista o un varargs
:
public static Sort by(List<Order> orders)
public static Sort by(Order... orders)
Este es el último Sort
de ejemplo de la sección anterior construido con objetos Order
:
List<Sort.Order> orders = List.of(Sort.Order.desc("countries"),
Sort.Order.asc("name"));
List<ConfederationSummaryRecord> confederationsSummary = confederationRepository.getSummaryCountryCount(Sort.by(orders));
Con todo, si revisas los métodos presentados, verás un detalle desconcertante: ¡ninguno permite configurar la capitalización! El constructor que recibe como argumento el valor de ignoreCage
es privado. La solución está en la API fluida de Order
, en concreto en el método ignoreCase
. Para usar la API, crea primero un Order
y luego haz que invoque a los métodos de esta tabla:
Sort.Order ignoreCase() | Devuelve un nuevo objeto Sort.Order activando el flag ignoreCase . |
Sort.Order nullsFirst() | Devuelve un nuevo objeto Sort.Order con la opción Sort.NullHandling.NULLS_FIRST . |
Sort.Order nullsLast() | Devuelve un nuevo objeto Sort.Order con la opción Sort.NullHandling.NULLS_LAST . |
Sort.Order nullsNative() | Devuelve un nuevo objeto Sort.Order con la opción Sort.NullHandling.NATIVE. Delega en el almacén de datos el criterio de ordenación de los nulos. |
Sort.Order with(Sort.Direction) | Devuelve un nuevo objeto Sort.Order con el sentido indicado. |
Sort.Order with(Sort.NullHandling) | Devuelve un nuevo objeto Sort.Order con el criterio para los nulos indicado. |
He aquí un ejemplo:
Sort.Order orderName = Sort.Order.by("name")
.with(Sort.Direction.DESC)
.ignoreCase();
La consecuencia de llamar a ignoreCase
es la aplicación la función LOWER
de JPQL para que las cadenas se ordenen como si todos sus caracteres estuvieran en minúsculas:
order by lower(c1_0.name) asc
¿Qué hacemos con los nulos?
Como dije, la respuesta a esta pregunta no la encontrarás en Sort
; pero tampoco en la especificación JPA.
Por fortuna, con Hibernate algo podemos hacer. HQL, su implementación de JPQL, admite las opciones NULLS FIRST
y NULLS LAST
de SQL:
ORDER BY c.name ASC NULLS LAST
Algunas bases de datos son compatibles con estas opciones (Oracle, PostgreSQL, HyperSQL), otras no (MySQL, SQL Server). En las últimas, Hibernate las emulará. Por ejemplo, en MySQL para NULLS FIRST
generará lo siguiente:
order by
case
when country0_.name is null then 0
else 1
end,
Cabe mencionar, además, la posibilidad de configurar la prioridad predeterminada de los nulos para todas las consultas que Hibernate genere:
spring.jpa.properties.hibernate.order_by.default_null_ordering=last
Los valores aceptados son first
(primero los nulos), last
(nulos al final) y none
(predeterminado, lo que haga la base de datos). Ten en cuenta que el comportamiento predeterminado de la base de datos depende de cada producto.
Ordenando con funciones
Un caso especial es la ordenación según el resultado de una función. Se trata de una capacidad propia de Spring Data JPA solo aplicable a las consultas declaradas con @Query
. Obliga a usar JpaSort
, un subtipo de Sort
tal y como muestra el siguiente diagrama.

Este Sort
ordena los países de mayor a menor según el tamaño de su nombre:
Sort sort = JpaSort.unsafe(Sort.Direction.DESC, "LENGTH(name)");
Cualquier cosa que contenga la cadena pasada como argumento a unsafe
se inserta en la consulta JPQL final generada por Spring Data JPA. En el proyecto del curso, Hibernate genera para la sentencia anterior este SQL:
order by character_length(c1_0.name) desc
Sort tipado con TypedSort<T>
Hay un elemento de inseguridad en las ordenaciones que hemos escrito: los atributos de las clases se indican con una cadena. Si nos equivocamos en su escritura, no detectaremos el error hasta que ejecutemos el código.
Nota. Si quieres constantes con los nombres de los atributos de las entidades, no las crees a mano. El plugin del metamodelo de JPA las crea automáticamente. Las tendrás siempre actualizadas y a prueba de refactorizaciones.
Spring Data solventa esta problemática con TypedSort<T>
, un «Sort
tipado para T
«. Consiste en una especialización de Sort
declarada como una clase interna, pública y estática de Sort
. Su razón de ser es requerir que los campos de ordenación se indiquen con referencias a métodos (method reference) de la clase T
, con lo que evitamos escribir cadenas.
Como puedes ver en el siguiente diagrama de clases, TypedSort
tiene métodos parecidos a los de Sort
que permiten configurar la ordenación de manera fluida.

Construyes un objeto de TypedSort
—su constructor es privado— con el método estático Sort#sort
.
public static TypedSort sort(Class type)
Un ejemplo de ordenación por dos campos:
Sort.TypedSort<ConfederationSummaryRecord> typedSort = Sort.sort(ConfederationSummaryRecord.class);
Sort sort = typedSort.by(ConfederationSummaryRecord::countries)
.descending()
.and(typedSort.by(ConfederationSummaryRecord::name));
List<ConfederationSummaryRecord> confederationsSummary = confederationRepository.getSummaryCountryCount(sort);
El objeto typedSort
exige establecer como campos de ordenación atributos de la clase ConfederationSummaryRecord
. Se indican proporcionando su método de acceso como argumento de by
.
Aunque el código compila, no podrás ejecutarlo debido a este error:
java.lang.IllegalArgumentException: Type to record invocations on must not be final
El mensaje es claro: el tipo (clase) no puede ser final. ¿Qué sentido tiene esta extraña restricción? Resulta que los objetos de TypedSort
necesitan internamente un objeto proxy de la clase de su tipado. Spring es incapaz de crear el proxy si su clase es final, y, como quizás ya sepas, los records
son clases finales. Por ello, voy a declarar este DTO para usarlo con TypedSort
:
package com.danielme.springdatajpa.model.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class ConfederationSummaryDTO {
private final Long id;
private final String name;
private final Long countries;
}
Ahora todo funciona:
Sort.TypedSort<ConfederationSummaryDTO> typedSort = Sort.sort(ConfederationSummaryDTO.class);
Sort sort = typedSort.by(ConfederationSummaryDTO::getCountries)
.descending()
.and(typedSort.by(ConfederationSummaryDTO::getName));
List<ConfederationSummaryRecord> confederationsSummary = confederationRepository.getSummaryCountryCount(sort);
Fíjate que el resultado de la consulta es ConfederationSummaryRecord
en vez de ConfederationSummaryDTO
(no existe relación alguna entre el tipado de typedSort
y el del resultado de getSummaryCountryCount
). Dado que esta dualidad resulta confusa, en este ejemplo aconsejo descartar el record
en favor del DTO. Ambas clases, al tener los mismos atributos, son intercambiables a la hora de recoger los resultados con solo ajustar la definicion de la consulta. En getSummaryCountryCount
tendrías que cambiar el constructor en la SELECT
:
@Query("""
SELECT new com.danielme.springdatajpa.model.dto.ConfederationSummaryRecord(
Prioridad de ordenaciones
¿Qué sucede si aplicamos Sort
a una consulta que ya tiene una ordenación declarada? Buena pregunta. Cambiemos la consulta de las últimas pruebas por esta:
@Query("""
SELECT new com.danielme.springdatajpa.model.dto.ConfederationSummaryRecord(
c.id, c.name as name, COUNT(ct.id) as countries)
FROM Country ct INNER JOIN ct.confederation c
GROUP BY ct.confederation.id, c.name
ORDER BY countries DESC""")
La sentencia JPQL contiene la ordenación por el número de países. Solicitemos la del nombre en un Sort
:
confederationRepository.getSummaryCountryCount(Sort.by("name"));
¡Funciona! Spring Data JPA combina las dos ordenaciones, añadiendo la del objeto Sort
a la establecida dentro de la propia consulta. Eso sí, cuando aparezcan criterios repetidos, las duplicidades no se eliminan.
El comportamiento anterior también se da en las consultas derivadas.
Un detalle curioso
Para cerrar el capítulo, te invito a revisar en la bitácora las consultas SQL que Hibernate genera para los ejemplos basados en consultas JPQL:
select c1_0.confederation_id,c2_0.name,count(c1_0.id)
from countries c1_0 join confederations c2_0 on c2_0.id=c1_0.confederation_id
group by c1_0.confederation_id,c2_0.name order by 3 desc,2 asc
¿Ves algo que te llame la atención? La cláusula ORDER BY
se refiere a los elementos de la proyección por su posición en vez de por su nombre. Se trata de una característica de SQL poco conocida, y no me sorprende: la consulta es menos legible y, sobre todo, frágil, porque un cambio en la SELECT
puede romper la ordenación si no se revisan las posiciones. En cualquier caso, es una consulta generada de forma automática, así que no te preocupes.
Resumen
Las claves del capítulo:
- Consigues ordenación dinámica agregando un parámetro
Sort
al método que presenta una consulta. Contiene un conjunto de criterios de ordenación basados en atributos de entidades y alias de elementos proyectados. - Creas objetos de tipo
Sort
con métodos estáticosby
y\o de manera fluida con la ayuda de otros métodos estáticos. Sort
almacena cada criterio de ordenación en unSort.Order
. Los métodos anteriores ya los crean, pero puedes crearlos con constructores y\o varios métodos estáticos.- Es posible emplear como campo de ordenación el resultado de una función gracias a
JpaSort#unsafe
. - La precedencia de los campos nulos contemplada por
Sort.Order
no funciona para JPA. Con Hibernate puedes hacer un par de cosas al respecto. TypedSort<T>
es una clase hija deSort
tipada para una claseT
. Te obliga a usar referencias a métodos deT
para establecer los campos de ordenación, lo cual resulta más seguro que usar cadenas.
Código de ejemplo
El proyecto, explicado en el capítulo dos, se encuentra en GitHub. Para más información sobre cómo utilizar GitHub consulta este artículo.
Otros tutoriales relacionados con Spring
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Spring Boot: Gestión de errores en aplicaciones web y REST
Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas