He destacado en varias ocasiones a lo largo del curso la importancia de recuperar de la base de datos solo la información imprescindible. En el capítulo siete aplicamos este principio a las proyecciones, e insistí en favorecer el empleo de interfaces, DTOs y records
frente a entidades. En el presente lo aplicaremos a la cantidad de registros a recuperar.
Índice
- El problema
- La paginación al rescate
- La interfaz Pageable
- Usando Pageable
- ¿Cómo lo hace Hibernate? El problema de OFFSET
- Obteniendo el detalle de la página
- Cuidado con JOIN FETCH
- Resumen
- Código de ejemplo
El problema
Cuanta más información recupere una consulta, mayor lentitud de la operación y consumo de memoria. El rendimiento del sistema se degrada y, en casos extremos, el sistema puede venirse abajo por un OutMemoryException
o similar. ¿Crees que exagero? Mira el método findAll
de CrudRepository
que recupera todas las entidades de un tipo; si en la tabla hubiera miles de registros, findAll
devolvería una lista gigantesca capaz de tumbar la aplicación. Además, quizás este problema pase desapercibido durante los primeros meses de uso del sistema porque existan pocos registros en la base de datos. Pero a medida que el tiempo pase, las tablas aumentarán de tamaño y en algún momento el problema nos explotará en la cara con toda su crudeza. Todo un peligro a evitar.
Admito que lo anterior es difícil de apreciar en el proyecto del curso: si la base de datos almacenara todos los países del mundo, findAll
devolvería unas doscientas entidades, algo nada problemático. No obstante, es un desperdicio de tiempo si no quieres disponer de todos los países en el mismo instante.
Pero el proyecto de ejemplo es un caso puntual; countries
es una tabla con un tamaño máximo de registros conocido. Lo habitual son las tablas de tamaño indefinido y susceptibles de crecer sin un límite predecible de antemano. Piensa en los pedidos de un comercio, las facturas emitidas por una empresa o los alquileres de películas del obsoleto ejemplo de un videoclub. Este tipo de tablas es el núcleo de las aplicaciones de gestión, así que resulta fundamental aprender a explotarlas con eficiencia.
La paginación al rescate
¿Cómo proceder en estos casos en los que una consulta podría recuperar muchos resultados?
Cuando hagas una búsqueda, no obtengas todos los resultados de una tacada. Agrúpalos en lotes (páginas) pequeños y manejables que vas trayendo de la base de datos de forma progresiva a medida que los necesites. La división en páginas debe realizarla la propia consulta en la base de datos, de forma que cada página se recupere ejecutando una nueva consulta.

Como usuario estás acostumbrado a emplear esta técnica de paginación en cualquier programa o sitio web que muestre un listado de datos, como un buscador. Date cuenta además de que en el caso del buscador será raro que consultes más allá de las dos primeras páginas de resultados; preferirás refinar los criterios de búsqueda.

En las aplicaciones para móviles la paginación es sutil. La información se muestra en un listado y las nuevas páginas se le van agregando con discreción a medida que lo deslizas.
La siguiente imagen ilustra la sección de paginación que propone Material Design. Su objetivo es que el usuario pueda «navegar» por las páginas correspondientes a los resultados de una búsqueda mostrados en una tabla.

Este componente gráfico permite indicar el tamaño de cada página (el número de filas de la tabla) y navegar entre ellas con botones. Este tamaño tendrá un límite máximo razonable y debes asegurar en el código que sea respetado. Asimismo, observa que un componente de este tipo precisa conocer el número total de resultados para calcular el total de páginas existentes. Volveremos sobre este requisito más adelante.
Subrayo un detalle crucial: para que el reparto de los datos en páginas sea coherente, en la consulta que va obteniendo las páginas SIEMPRE debe aplicarse el mismo tamaño de página y, algo menos evidente, criterio de ordenación. Esto último se debe a que las bases de datos nunca garantizan un orden predeterminado.
Nota. Para aplicar paginación con JPA sin Spring Data consulta este capítulo del curso Jakarta EE 9:
Por último, un inconveniente de la estrategia de paginación que acabo de presentarte. Si se añaden nuevos registros mientras navegamos por las páginas y estos deberían aparecer en las páginas precedentes a las que estamos solicitando, los resultados pueden ser inconsistentes. Por lo común, podemos ignorar este pequeño defecto.
La interfaz Pageable
¿Cómo se programa lo anterior con Spring Data JPA? Debería ser sencillo, pues la técnica de paginación es de aplicación rutinaria en nuestro día a día…
Lo tienes fácil. Consigues que un método de un repositorio pagine sus resultados procediendo de manera análoga a cómo lo haces para que admita la ordenación dinámica explicada en el capítulo previo. Tienes que añadir al método un parámetro de cierto tipo: si en el caso de la ordenación ese parámetro es de la clase Sort
, para la paginación es la interfaz Pageable
que modela la configuración que Spring Data precisa para paginar los resultados. Esta praxis es válida con independencia del tipo de consulta que ejecute el método (derivada, JPQL, QBE, Criteria, SQL).
Nota. Sort
y Pageable
pertenecen a Spring Data Commons. Por tanto, no son específicas de Spring Data JPA y pueden utilizarse con otras tecnologías de almacenamiento de datos.
Aquí tienes un primer ejemplo:
List<Country> findAllByOrderByName(Pageable pageable);
Como es natural, la interfaz Pageable
requiere de una implementación. Spring Data provee la clase PageRequest
.

PageRequest
se apoya en tres atributos para cumplir con el contrato definido por la interfaz:
page
. El número de la página. Empieza en cero.size
. El tamaño máximo de la página (los resultados que contiene).sort
. Los criterios de ordenación.
¿Cómo creamos un objeto de PageRequest
con los valores que queramos para estos atributos si su único constructor está protegido? Estamos ante el mismo caso de Sort
: PageRequest
nos obliga a utilizar varios «constructores estáticos». En esta ocasión, los métodos tienen el prefijo «of» en lugar de «by». El más simple (método ofSize
) solo requiere el tamaño de la página, la cual se refiere a la primera (la cero):
public static PageRequest ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
Los métodos «of» restantes exigen como mínimo el número de página y su tamaño máximo:
public static PageRequest of(int page, int size) {
return of(page, size, Sort.unsorted());
}
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
public static PageRequest of(int page, int size, Direction direction, String... properties) {
return of(page, size, Sort.by(direction, properties));
}
Si el número de página es menor que cero, se lanza una excepción:
java.lang.IllegalArgumentException: Page index must not be less than zero
Ídem si el tamaño inferior a uno:
java.lang.IllegalArgumentException: Page size must not be less than one
Los dos últimos métodos «of» son los que probablemente llamarás con mayor frecuencia. Reciben la ordenación mediante un argumento Sort
, o bien con un varargs
con los campos de ordenación y un Direction
con el sentido.
Aunque puedes crear objetos Pageable
sin ordenación, asegúrate de que la consulta aplique un orden, tal y como ya señalé. Cuando no sepas qué orden aplicar o el solicitante de los datos no lo especifique (si es que le das esa opción), establece una ordenación predeterminada que tenga sentido para los datos con los que trates.
En el siguiente ejemplo la propia consulta contiene la ordenación, así que no hace falta definirla en el objeto pageable
. En caso de hacerlo, la ordenación de pageable
se agrega a la de la consulta.
List<Country> findAllByOrderByName(Pageable pageable);
Cabe destacar que esto es incorrecto:
List<Country> findAll(Pageable pageable, Sort sort);
El método causa una excepción:
Reason: Method must not have Pageable *and* Sort parameters. Use sorting capabilities on Pageable instead;
El mensaje de error explica que está prohibido declarar un parámetro Sort
y otro Pageable
en el mismo método. En vez de ello, cuando la ordenación sea dinámica debes incluirla en el objeto Pageable
con los métodos «of» adecuados.
Usando Pageable
En los repositorios que hereden de PagingAndSortingRepository encontrarás un método findAll
que obtiene con paginación todas las entidades del tipo de un repositorio.

¡Manos a la obra! El caso de prueba es este:
«Obtener la primera página correspondiente a los países ordenados por su nombre en sentido ascendente, asumiendo que el tamaño de la página es cuatro».
Ten en cuenta que, como se explicó en el capítulo dos, el juego de datos de prueba consta de doce países.
Si esta es la consulta derivada (ya mostrada párrafos atrás):
List<Country> findAllByOrderByName(Pageable pageable)
estos dos argumentos son equivalentes:
Pageable page1 = PageRequest.ofSize(4);
Pageable page1 = PageRequest.of(0, 4);
Si eliminas la ordenación del nombre del método:
List<Country> findAll(Pageable pageable);
estos Pageable
aseguran los resultados anteriores:
Pageable page1 = PageRequest.of(0, PAGE_SIZE, Sort.by("name"));
Pageable page1 = PageRequest.of(0, PAGE_SIZE, Sort.Direction.ASC, "name");
Si quisieras invocar a findAll
sin paginación, crea una paginación indefinida con Pageable.unpaged()
:
List<Country> countries = countryRepository.findAll(Pageable.unpaged());
De todas maneras, esto contraviene el objetivo de la paginación. Sopesa si de verdad quieres hacerlo.
Puedes encadenar los métodos de PageRequest
que retornan su misma clase para configurar el objeto de manera fluida:
Pageable pageable = PageRequest.ofSize(PAGE_SIZE)
.withPage(0)
.withSort(Sort.by("name"));
Al ser PageRequest
inmutable, estos métodos devuelven un nuevo objeto.
También es posible navegar por las páginas gracias a los métodos de Pageable
comentados en esta tabla:
PageRequest first() | Devuelve un nuevo PageRequest configurado para la primera página. |
PageRequest next() | Devuelve un nuevo PageRequest configurado para la siguiente página. |
PageRequest previous() | Devuelve un nuevo PageRequest configurado para la página previa. Si el PageRequest que invoca a previous representa a la primera página, previous devuelve ese mismo PageRequest . |
PageRequest previousOrFirst() | Igual que previous , pero el nombre es más revelador. |
PageRequest withPage(int) | Devuelve un nuevo PageRequest configurado para la página indicada. Recuerda que la primera página es la cero. |
Sirva esta prueba como demostración práctica de algunos de los métodos de la tabla. Obtiene la primera página (countriesFirstPage
), la siguiente (pageablePageNext
) y la número cuatro (pageablePage4
, sin datos porque solo hay doce países en la tabla):
@Test
void testFindAllListNavigation() {
Pageable pageable = PageRequest.of(0, PAGE_SIZE, Sort.Direction.ASC, "name");
List<Country> countriesFirstPage = countryRepository.findAll(pageable);
assertThat(countriesFirstPage)
.extracting(Country::getId)
.containsExactly(COLOMBIA_ID, COSTA_RICA_ID, GUATEMALA_ID, MEXICO_ID);
Pageable pageablePageNext = pageable.next();
List<Country> countriesPageTwo = countryRepository.findAll(pageablePageNext);
assertThat(countriesPageTwo)
.extracting(Country::getId)
.containsExactly(NORWAY_ID, PERU_ID, KOREA_ID, SPAIN_ID);
Pageable pageablePage4 = pageable.withPage(4);
List<Country> countriesPageFour = countryRepository.findAll(pageablePage4);
assertThat(countriesPageFour).isEmpty();
}
¿Cómo lo hace Hibernate? El problema de OFFSET
Activando las trazas descubrirás en la bitácora cómo Hibernate se las apaña para obtener los datos en páginas:
select c1_0.id,c1_0.capital,c1_0.confederation_id,c1_0.name,c1_0.ocde,c1_0.population,c1_0.united_nations_admission from countries c1_0 order by c1_0.name asc
offset ? rows fetch first ? rows only
Al final de la consulta se indican los resultados a omitir (offset
) hasta llegar al primero a devolver, y la cantidad de resultados deseados (first
) a partir de ese primero (inclusive). El SQL es el correspondiente a nuestra base de datos de pruebas (HyperSQL) y respeta la sintaxis que propone el estándar SQL:2008. En otras bases de datos tal vez cambien los nombres de las opciones, pero será parecido. Para MySQL Hibernate genera esto:
order by country0_.name asc limit ?, ?
Lo que quiero resaltar es que la paginación es lo más eficiente posible: Hibernate solo recupera los registros requeridos.
Ahora bien, el empleo de OFFSET
o equivalente presenta un inconveniente en lo tocante al rendimiento: cuanto mayor sea el número de la página solicitada, más lenta es la consulta por ser cada vez mayor el valor de OFFSET
. Esto se debe a que la base de datos lee secuencialmente todos los resultados de la consulta hasta alcanzar al señalado por OFFSET
, momento en el que devolverá los resultados solicitados.
Supongamos que pides la primera página de una consulta, siendo cien el tamaño de cada página. En ese caso, OFFSET
es cero; ningún problema. Si reclamas la página cincuenta, OFFSET
vale cinco mil, lo que obliga a la base de datos a leer cinco mil registros antes de darte los resultados. Obtener la página cincuenta resulta, pues, bastante más lento que obtener la primera.
En resumen, cada vez que avanzas de página el rendimiento es peor. Si esto supusiera un problema importante, puedes investigar técnicas como keyset pagination (claves de paginación) que quedan fuera del alcance de este modesto curso. Si hablamos de una interfaz gráfica y no quieres complicarte la vida, quizás te convenga simplemente limitar el número máximo de página que el usuario puede solicitar e informarle de esta restricción. A fin de cuentas, las primeras páginas son las relevantes.
Obteniendo el detalle de la página
Llegado a este punto, has aprendido a obtener los resultados de las consultas de forma paginada. Sin embargo, falta un detalle al que hice referencia párrafos atrás: averiguar las páginas existentes. O lo que es lo mismo, la cantidad total de resultados. Necesitas ese dato para facilitar la navegación por los resultados y, por ejemplo, construir la barra de navegación que mostré al principio del capítulo.
Como ya sabrás, obtienes el conteo de los resultados de una consulta escribiendo una consulta derivada cuyo nombre comience con la expresión count...By
:
long countByConfederationId(Long id);
En JPQL, igual que en SQL, con la función de agregación COUNT
:
select count(c) from Country c WHERE c.confederation.id = :confederationId
En consecuencia, cada búsqueda requiere dos consultas: la que obtiene los resultados página a página y la que cuenta los resultados existentes.
La interfaz Page<T>
Por fortuna, Spring Data se encarga de la consulta de conteo, así como de utilizar el resultado para calcular el total de páginas. Tan sencillo como hacer que el método que representa a la consulta paginada devuelva la interfaz Page<T>
. Aparece arriba a la derecha del siguiente diagrama de clases.

Page<T>
está tipada para el resultado «real» de la consulta: T
es la clase de la entidad, el record
, el DTO, etcétera, que corresponda. Accedes a los resultados pertenecientes a la página, una lista de T
, llamando a getResult
.
La utilidad de Page
radica en que contiene los detalles de la página devuelta por la consulta, incluyendo el número de la página actual y el total de ellas. Incluso provee los objetos Pageable
y Sort
empleados. Y todo ello sin requerirse configuración adicional alguna en el método del repositorio.
Esta tabla recopila los métodos de Page
más interesantes:
long getTotalElements() | El número total de resultados (el conteo). |
int getTotalPages() | El número total de páginas. |
int getSize() | El tamaño de la página. |
int getNumber() | La página que representa el objeto Page . |
int getNumberOfElements() | El número de resultados de la página. Si se trata de la última, no tienen por qué coincidir con el tamaño de la página (el método getSize() ). |
boolean isFirst() | Indica si la página es la primera. |
boolean isLast() | Indica si la página es la última. |
boolean hasNext() | Indica si hay más páginas. |
boolean hasPrevious() | Indica si no es la primera página. |
Pageable getPageable() | El Pageable usado. |
Juguemos con las capacidades de Page
con esta consulta derivada:
Page<Country> findPageBy(Pageable pageable);
De nuevo, una prueba que obtiene la primera página, de tamaño cuatro, con los países ordenados por nombre:
@Test
void testFindAllPage() {
Pageable pageable = PageRequest.of(0, PAGE_SIZE, Sort.Direction.ASC, "name");
Page<Country> countriesFirstPage = countryRepository.findPageBy(pageable);
assertCountriesFirstPage(countriesFirstPage.getContent());
assertThat(countriesFirstPage.getTotalElements()).isEqualTo(12);
assertThat(countriesFirstPage.getNumber()).isZero();
assertThat(countriesFirstPage.getTotalPages()).isEqualTo(3);
assertThat(countriesFirstPage.getNumberOfElements()).isEqualTo(PAGE_SIZE);
assertThat(countriesFirstPage.getSize()).isEqualTo(4);
}
No hay mucho que explicar acerca del método testFindAllPage
, un pretexto para que veas en acción algunos métodos de Page
.
En cualquier caso, la clave para que todo funcione reside en la sentencia SELECT COUNT
que Spring Data JPA genera y ejecuta para conocer todos los posibles resultados —el valor que devuelve getTotalElements
—. La verás en la bitácora. Para el ejemplo anterior tenemos esto:
select c1_0.id,c1_0.capital,c1_0.confederation_id,c1_0.name,c1_0.ocde,c1_0.population,c1_0.united_nations_admission from countries c1_0
order by c1_0.name asc offset ? rows fetch first ? rows only
select count(c1_0.id) from countries c1_0
En el caso de las consultas JPQL declaradas con @Query
es posible indicar la consulta de conteo en la propiedad countQuery
de la anotación, o bien el nombre de una consulta nombrada con ese cometido en la propiedad countName
:
@Query(value="SELECT c FROM Country c",
countQuery = "SELECT count(c) FROM Country c")
Page<Country> findPageWithJpql(Pageable pageable);
El método findPageWithJpql
equivale a findPageBy
. Un ejemplo teórico que no tiene sentido; mejor quedarse con la consulta derivada, o quitar countQuery
por innecesaria. Solo tendrás que declarar la consulta de conteo en countQuery
si usas @Query
para declarar consultas con el lenguaje SQL (el tema del capítulo doce), o bien en el raro caso de que Spring Data JPA sea incapaz de generar la consulta de conteo (veremos un ejemplo en breve).
La interfaz Slice<T>
Si no necesitas el número total de resultados\páginas —circunstancia poco habitual según mi experiencia—, tus métodos pueden devolver Slice
(porción), la interfaz padre de Page
. Con ella evitas la ejecución de la consulta de conteo. Al no disponer de su resultado, Slice
carece de los métodos getTotalPages
y getTotalElements
que sí ofrece Page
. Los demás métodos de Page
de la tabla que vimos siguen disponibles en Slice
(de hecho, Page
los hereda de Slice
).
He aquí una consulta derivada que devuelve Slice
:
Slice<Country> findSliceBy(Pageable pageable);
Esta prueba es la misma que la anterior sin las aserciones sobre los métodos getTotalPages
y getTotalElements
:
@Test
void testFindAllSlice() {
Pageable pageable = PageRequest.of(0, PAGE_SIZE, Sort.Direction.ASC, "name");
Slice<Country> countriesFirstPage = countryRepository.findSliceBy(pageable);
assertCountriesFirstPage(countriesFirstPage.getContent());
assertThat(countriesFirstPage.getNumber()).isZero();
assertThat(countriesFirstPage.getNumberOfElements()).isEqualTo(PAGE_SIZE);
assertThat(countriesFirstPage.getSize()).isEqualTo(4);
}
Cuidado con JOIN FETCH
Ahora, una mala noticia. Debo advertirte de un caso en el que Spring Data JPA e Hibernate tienen dificultades al paginar los resultados. Se trata de las consultas JPQL con el tipo de reunión JOIN FETCH
. En esencia, esta reunión une dos entidades de modo que la entidad que se devuelve como resultado incluye la relación perezosa por la que se ha efectuado la reunión.
Veamos un ejemplo. Country
tiene una relación con Confederation
:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "confederation_id")
private Confederation confederation;
Al ser la relación perezosa (lazy), las entidades de los países que recuperes no contienen la confederación. Ésta se obtiene de la base de datos la primera vez que accedes a uno de sus atributos que no sea el identificador. Si desconoces el porqué de este peculiar comportamiento, de nuevo te remito a esta publicación.
La cuestión es que para obtener la confederación se lanza una consulta adicional a la que trae el país. Si haces esto con los países de un listado uno por uno, incurres en una penalización del rendimiento (el infame problema denominado N + 1). En general, no obtengas relaciones perezosas con los métodos getters; plantéate siempre traer las entidades minimizando las llamadas a la base de datos.
Con esta consulta los países sí contendrán la confederación; lo recuperas todo en una sola operación:
SELECT c
FROM Country c JOIN FETCH c.confederation
Escribamos entonces el método que la ejecuta:
@Query(value="SELECT c FROM Country c JOIN c.confederation")
Page<Country> findAllWithConfederation(Pageable pageable);
¡No funciona! Spring Data JPA genera una consulta de conteo incorrecta, al menos en la versión 3.0. La validación de esa consulta causa una excepción:
org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract org.springframework.data.domain.Page com.danielme.springdatajpa.repository.query.CountryPagingRepository.findAllWithConfederation(org.springframework.data.domain.Pageable); Reason: Count query validation failed for method public abstract org.springframework.data.domain.Page com.danielme.springdatajpa.repository.query.CountryPagingRepository.findAllWithConfederation(org.springframework.data.domain.Pageable) org.hibernate.query.SemanticException: query specified join fetching, but the owner of the fetched association was not present in the select list
Toca escribirla:
@Query(value="SELECT c FROM Country c JOIN c.confederation",
countQuery = "SELECT COUNT(c) FROM Country c")
Page<Country> findAllWithConfederation(Pageable pageable);
Ahora todo está en orden. Pero si utilizas JOIN FETCH
con una relación múltiple, existe otro problema. Observa la relación bidireccional entre Country
y Confederation
:
public class Confederation {
@OneToMany(mappedBy = "confederation")
private List<Country> countries;
Nota. Las relaciones @OneToMany
son perezosas de manera predeterminada.
Reunamos las confederaciones con sus países un nuevo repositorio:
@Transactional(readOnly = true)
public interface ConfederationPagingRepository extends Repository<Confederation, Long> {
@Query(value="SELECT c FROM Confederation c JOIN FETCH c.countries",
countQuery = "SELECT COUNT(c) FROM Confederation c")
Page<Confederation> findAllWithCountry(Pageable pageable);
Funciona…pero no como sería deseable. En la bitácora verás esta advertencia:
WARN org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
Nota. Por si te lo estás preguntando, firstResult
y maxResults
son los métodos de la interfaz Query
de JPA con los que se establece la paginación de las consultas JPQL y SQL.
Hibernate no puede paginar con la consulta SQL, así que decide recuperar todos los resultados de la base de datos y extraer los de la página requerida. Queda claro revisando la consulta realizada ya que no aparece la expresión offset-limit:
select c1_0.id,c2_0.confederation_id,c2_0.id,c2_0.capital,c2_0.name,c2_0.ocde,c2_0.population,c2_0.united_nations_admission,c1_0.name
from confederations c1_0 join countries c2_0 on c1_0.id=c2_0.confederation_id
order by c1_0.name asc
La razón de esta limitación se halla en la proyección de la SELECT
. Fíjate bien: no consiste en la confederación, sino en los emparejamientos entre las confederaciones y los países. Si Hibernate paginase esta proyección, estaría paginando esos emparejamientos y no las confederaciones, por lo que los resultados serían estrambóticos. Por ejemplo, quizás recuperes confederaciones que no contengan todos los países.
Puesto que este comportamiento es contrario a lo que perseguimos con la paginación, es menester encontrar una alternativa, suponiendo que JOIN FETCH
sea necesario. La solución es bien conocida. Aquí la tienes adaptada al ejemplo:
- Obtén los identificadores de las confederaciones correspondientes a la página deseada:
@Query("select c.id from Confederation c")
Page<Long> findConfederationsWithCountry(Pageable page);
- Selecciona con ellos los resultados de la consulta que tiene el
JOIN FETCH
:
@Query(value = "SELECT c FROM Confederation c JOIN FETCH c.countries where c.id IN (:ids)")
List<Confederation> findAllConfederation(List<Long> ids, Sort sort);
- Combina los dos métodos y construye el resultado con
PageImpl
. Puedes hacerlo en el repositorio con un métododefault
:
default Page<Confederation> findAllWithCountryPagingTwoQueries(Pageable pageable) {
Page<Long> idsPage = findConfederationsWithCountry(pageable);
List<Confederation> confederationsPage1 = findAllConfederation(idsPage.getContent(), idsPage.getSort());
return new PageImpl<>(confederationsPage1, pageable, idsPage.getTotalElements());
}
¡Tachán! Costó algo de trabajo, pero mereció la pena. Ejecutar dos consultas —en realidad tres con la de conteo— en esta circunstancia ofrecerá un mejor rendimiento que paginar en memoria cuanto mayor sea el número total de resultados.
Resumen
Los fundamentos del capítulo:
- Aplicas la técnica clásica de paginación agregando un parámetro de la interfaz
Pageable
a los métodos de los repositorios. - Creas objetos
Pageable
con los métodos estáticos de la clasePageRequest
. - Recuerda imponer un criterio de ordenación para garantizar la coherencia de los resultados. Cuando navegues por las páginas, usa el mismo orden y tamaño de página.
- Aunque los métodos con
Pageable
pueden devolver los tipos «de siempre», en general querrás la interfazPage
como tipo de retorno. Así, conocerás los detalles de la paginación. - Cuando devuelves
Page
, Spring Data JPA ejecuta una consulta que cuenta todos los resultados. Declara esa consulta con la propiedadcountQuery
si Spring es incapaz de generarla automáticamente. - Si no necesitas el total de resultados, recurre a
Slice
, la interfaz padre dePage
. Te ahorras la consulta de conteo. - Ojo avizor con la paginación de consultas con reuniones
JOIN FETCH
.
Código de ejemplo
El proyecto, explicado en el capítulo dos, se encuentra en GitHub. Para más información acerca de 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