Paginación de resultados

PanacheQuery nos permite cuidar más la query que queremos que se mande a la base de datos, aprovechando el poder de Hibernate ORM, que es lo que lleva debajo. En este vídeo veremos cómo aplicar paginación a una query para poder ofrecer menos resultados por endpoint y así no someter a tanto estrés a la base de datos.

Hasta ahora solamente hemos usado el método listAll() o list() para recuperar todos los elementos de una tabla. Sin embargo, si nuestra tabla tuviese miles de registros, eso supondría devolver un array de varios miles de objetos JSON, algo que puede saturar al servidor al generar mucha información, además de saturar la base de datos por trabajar demasiado. En su lugar, lo normal es paginar la respuesta.

La paginación de una consulta de base de datos consiste en dividir los registros a devolver en páginas, que son pequeños grupos de información de un tamaño fijo. Por ejemplo, si tenemos que devolver 1200 registros, los podríamos agrupar en páginas de un tamaño de 25 elementos, lo que significa que como mucho devolveremos 25 elementos en el JSON de un endpoint, y se traerá de base de datos solamente 25 elementos.

Mediante un selector de página podremos especificar qué conjunto de elementos queremos traer. Por ejemplo, para la primera página devolveríamos los primeros 25 elementos; para la segunda página devolveríamos los siguientes 25 elementos; para la tercera los siguientes 25, y así con el resto de páginas, hasta llegar a la última, con los últimos 25 elementos, como si fuesen las filas de una tabla que hemos impreso a múltiples páginas.

Para paginar en Quarkus, tendremos antes que obtener una instancia de PanacheQuery, que es algo que podemos hacer con métodos como findAll()o find(). Estos métodos son los que inician una de estas queries, que son clases que podemos usar luego para manipular cada aspecto de la query. Por ejemplo:

PanacheQuery<Book> query = repository.findAll();

A partir de aquí, podemos usar el método .page() para acotar a una página concreta de resultados. Esto lo podemos hacer de dos modos:

  • Utilizando una llamada a page(int pageIndex, int pageSize). Con esos dos parámetros de tipo int le podemos indicar el número de página y el número de elementos por página. Por ejemplo, si tuviesemos una tabla de 3500 resultados y nos interesase dividirlos en grupos de 50 elementos por página, podríamos usar page(0, 50) para devolver los primeros 50 elementos; y page(1, 50) para devolver los siguientes 50, y así con todos. A tener en cuenta que en Quarkus las páginas empiezan a contar en 0, a diferencia de un libro de papel donde probablemente empezaría en 1.
  • O bien directamente usando una llamada a page(Page page), que acepta un objeto de tipo Page. Generalmente son los mismos parámetros pero envueltos en una clase, con algunas utilidades como la posibilidad de navegar a la primera, última, anterior o siguiente página usando métodos como first, last, previous y next. Para instanciar estas páginas podemos usar alguno de los constructores de esa clase o bien alguno de los métodos estáticos de la misma.

A modo de ejemplo, si quisiésemos obtener la primera página de resultados lo podríamos hacer así:

PanacheQuery<Book> query = repository.findAll();
query.page(Page.ofSize(20).index(0));

Existen muchísimas otras formas de especificarle la página, todas equivalentes entre sí:

  • query.page(0, 20)
  • query.page(new Page(0, 20))
  • query.page(Page.of(0, 20))

La llamada al método page es mutable, es decir, modificará propiedades internas de la query. En este caso, la variable query representa la query que estamos preparando, y una llamada a ese método cambiará la ventana que se devuelve. Esto es muy importante, y una diferencia con respecto a otros frameworks de acceso a datos de otros lenguajes de programación, donde los métodos devuelven copias de la query. En Panache no ocurre esto, incluso aunque los métodos se devuelvan a si mismo para poder encadenarlos.

Por otro lado, cuando queramos transformar una PanacheQuery en una lista de resultados podemos llamar al método list. Por ejemplo, el siguiente ejemplo nos devolvería una lista que contendrá hasta 25 elementos con los primeros 25 elementos de la relación de libros.

PanacheQuery<Book> query = repository.findAll();
query.page(Page.of(0, 20));
List<Book> books = query.list();

Si olvidásemos llamar a page, el método list seguiría funcionando, pero en este caso en vez de hasta 25 elementos podría devolverlos todos. De ahí el caracter mutable de PanacheQuery, porque las llamadas que hagamos a sus métodos podrían modificar el estado interno, que se conservará entre llamada y llamada. Para paginar esto no será útil, pero en el capítulo donde hablo sobre los filtros de una de estas queries sí será más relevante.

Por último, indicar que una PanacheQuery también puede ser instanciada con un criterio de ordenación, que se aplicará junto a todos los demás criterios y modificaciones que hayamos puesto a esta variable.

Con todo esto, aquí hay un ejemplo completo de uno de estos endpoints:

@GET
public List<Book> listBooks(@QueryParam("page") @DefaultValue("1") int page) {
  Sort recentIds = Sort.descending("id");
  var query = booksRepository.findAll(recentIds);
  query.page(page - 1, 20);
  return query.list();
}

Para indicar el número de página estoy usando el atributo QueryParam. Así, se puede especificar mediante un query param si queremos la primera página (/books?page=1), la segunda página (/books?page=2) o la página número 14 (/books?page=14). Empezamos a contar en 1 para hacerlo más natural, y esa es la razón por la que cuando llamo al método PanacheQuery.page le resto 1, para alinearlo con los números que espera Panache, que empiezan a contar en 0. Al queryparam page, además, le estoy poniendo un valor por defecto mediante el uso de la anotación @DefaultValue("1"), donde le puedo especificar el valor que quiero que tenga el queryparam si no se le ha dado uno desde la URL. En caso de no especificar ninguno, el parámetro tendría como valor por defecto null (o en este caso, al ser de tipo int, solamente un 0).

Este otro ejemplo, más avanzado, devuelve un objeto JSON con formato que muestra el número de página actual y el número de páginas, y está basado parcialmente en este record llamado PaginatedResponse que todo lo que hace es formatear un PanacheQuery. Puedes encontrarlo aquí: https://gist.github.com/danirod/f420586f82a2ee17072258fc322ae291.

@GET
public PaginatedResponse listBooks(@QueryParam("page") @DefaultValue("1") int page) {
  Sort recentIds = Sort.descending("id");
  var query = booksRepository.findAll(recentIds);
  query.page(page - 1, 20);
  return new PaginatedResponse(query);
}
Lista de reproducción
  1. 1
    Configurar una base de datos
    7 minutos
  2. 2
    Crear una entidad y un repositorio
    12 minutos
  3. 3
    Active Record con PanacheEntity
    4 minutos
  4. 4
    Modificar y borrar registros
    12 minutos
  5. 5
    Filtros y ordenación
    12 minutos
  6. 6
    Paginación de resultados
    13 minutos
  7. 7
    Aplicar filtros dinámicos
    12 minutos
  8. 8
    JsonIgnore, JsonProperty y JsonAlias
    6 minutos