Curso Spring Data JPA. 9: paginación

logo spring

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

  1. El problema
  2. La paginación al rescate
  3. La interfaz Pageable
  4. Usando Pageable
  5. ¿Cómo lo hace Hibernate? El problema de OFFSET
  6. Obteniendo el detalle de la página
    1. La interfaz Page<T>
    2. La interfaz Slice<T>
  7. Cuidado con JOIN FETCH
  8. Resumen
  9. 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:

  1. 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);
  1. 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);
  1. Combina los dos métodos y construye el resultado con PageImpl. Puedes hacerlo en el repositorio con un método default:
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 clase PageRequest.
  • 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 interfaz Page 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 propiedad countQuery si Spring es incapaz de generarla automáticamente.
  • Si no necesitas el total de resultados, recurre a Slice, la interfaz padre de Page. 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

Spring JDBC Template: simplificando el uso de SQL

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.