Paginación en Servicios Rest
Con esta guía vamos a presentar como realizar la paginación en APIs de servicios Rest. ¿Que es la paginación? La paginación es realizar una consulta sobre una colección de entidades (Tablas de BBDD, etc.), donde la consulta solo devuelve un número determinado de elementos y no todos los elementos, a la que llamaremos página. También se proporcionaba los medios para poder obtener: la primera página, la página anterior, la página posterior, la última página, el número total de páginas y el número total de elementos que obtiene la consulta. Esta consulta se puede ordenar y filtrar.
Configuración
Para poder trabajar con la paginación en los servicios Rest en aplicaciones FundeWeb 2.0, tenemos que añadir la siguiente configuración en el fichero web.xml:
<context-param> <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name> <param-value> org.glassfish.jersey.pagination.PaginationContainerResponseFilter </param-value> </context-param>
Recordemos que el parámetro de contexto com.sun.jersey.spi.container.ContainerResponseFilters se puede indicar el valor como una lista separada por ','. Por lo que si ya existe dicha declaración solo hay que añadir el org.glassfish.jersey.pagination.PaginationContainerResponseFilter.
Para poder trabajar con la paginación en los servicios Rest en aplicaciones FundeWeb 1.5 migradas a Weblogic 12.2, tenemos que añadir la siguiente configuración en el fichero cxf-beans.xml:
<beans ...> <jaxrs:server ...> ... <jaxrs:providers> <ref bean="paginationContainerResponseFilter"/> ... </jaxrs:providers> </jaxrs:server> <bean id="paginationContainerResponseFilter" class="org.umu.atica.jaxrs.pagination.PaginationContainerResponseFilter"/> ... </beans>
Si utilizamos el gestor de problemas, paginationContainerResponseFilter tiene que estar declarado antes que problemContainerResponseFilter.
Aunque no sea estrictamente necesario, os recomendamos configurar Jackson como proveeder de JSON. Aunque tambien se puede con Jettison.
Ejemplos
Ejemplo cuando solo devolvemos JSON y sin ordenación ni filtrado
Veamos un ejemplo para obtener los roles de una aplicación donde el resultado solo produce JSON:
@GET @Path( "/roles" ) @Produces( MediaType.APPLICATION_JSON ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit) { PaginatedEntity<List<RolDto>> pe = new PaginatedEntity<List<RolDto>>(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), null); pe.setCurrentPage( datos.getResultList() ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
- La petición tiene que ser GET como suele ser habitual en las APIs Rest para realizar consultas.
- La petición devuelve JSON.
- Tiene como parámetros: page (el número de página) y limit (el número de elementos a devolver en la página).
- La petición devuelve un objeto Response con la entidad PaginatedEntity. Este objeto se crea pasando el número de pagina, el limite de elementos por página.
- Antes de devolver el objeto Response, tenemos que establecer en la instancia de la clase PaginatedEntity, los elementos que devuelve la consulta y el número total de elementos.
- En la clase PaginatedEntity, el parámetro de clase es un objeto de List<RolDto>, donde RolDto es un POJO con la información del rol. El parámetro de clase también podría ser del tipo RolDto[].
- Tiene dos constructores, que tienen como parámetros: el indice de la página, el número de elementos para cada página o la ordenación de la consulta.
Veamos ahora la llamada al servicio de roles:
public ResultQuery<RolDto> obtenerRolDtoPaginados(Map<String, Object> parameters, int firstResult, int resultLimit, String order) { return rolDtoDataAccessService.obtenerRolDtoPaginados(parameters, firstResult, resultLimit, order); }
El trabajo real se hace en el DAS para el RolDto:
package es.um.atica.prueba.security.authorization.das.interfaces; import java.util.Map; import es.um.atica.jpa.das.DataAccessService; import es.um.atica.jpa.das.ResultQuery; import es.um.atica.prueba.services.rest.pojo.RolDto; public interface RolDtoDataAccessService extends DataAccessService<RolDto> { static final String NAME = "rolDtoDataAccessService"; ResultQuery<RolDto> obtenerRolDtoPaginados(Map<String, Object> parameters, int firstResult, int resultLimit, String order); }
La implementación:
package es.um.atica.prueba.security.authorization.das.impl; import java.util.Arrays; import java.util.Map; import javax.ejb.Local; import javax.ejb.Stateless; import org.jboss.seam.annotations.Name; import es.um.atica.commons.utils.UtilString; import es.um.atica.jpa.das.DataAccessServiceImpl; import es.um.atica.jpa.das.ResultQuery; import es.um.atica.prueba.security.authorization.das.interfaces.RolDtoDataAccessService; import es.um.atica.prueba.security.authorization.entities.Roles; import es.um.atica.prueba.services.rest.pojo.RolDto; @Local( RolDtoDataAccessService.class ) @Stateless @Name( RolDtoDataAccessService.NAME ) public class RolDtoDataAccessServiceImpl extends DataAccessServiceImpl<RolDto> implements RolDtoDataAccessService { private static final String[] RESTRICTIONS_ROL_DTO = { UtilString.cmpTextFilterEjbql("rol.idRol", ":idRol"), }; @Override public ResultQuery<RolDto> obtenerRolDtoPaginados( Map<String, Object> parameters, int firstResult, int resultLimit, String order ) { return resultsByDtoNamedQueryWithDinamicFilter( Roles.ROLE_DTO_ALL, Arrays.asList(RESTRICTIONS_ROL_DTO), parameters, firstResult, resultLimit, order, null, null ); } }
Con la llamada al método resultsByDtoNamedQueryWithDinamicFilter(), mapeamos los datos de la tabla en un DTO. La consulta la tenemos definida en la entidad Roles. Las propiedades del DTO tienen que tener el mismo nombre del alias que tiene en la consulta JPQL.
En estos ejemplos hacemos un mapeo a un DTO partiendo de una consulta JPQL, pero este mapeo también se puede realizar con consultas nativas. Como por ejemplo resultsByDtoNamedNativeQueryWithDinamicFilter().
@NamedQuery(name = Roles.ROLE_DTO_ALL, query = "select rol.idRol as idRol, rol.rolPadre.idRol as rolPadre, rol.descripcion as descripcion from Roles rol") ... public static final String ROLE_DTO_ALL = "Roles.obtenerTodosRolDto";
En la clase RolDto tenemos que tener definidas las propiedades: idRol, rolPadre y descripcion; con sus respectivos métodos de acceso (get/set).
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5
Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 1 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="last"
Estos datos son así, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
Ejemplo cuando devolvemos JSON o XML y sin ordenación ni filtrado
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1']", page, limit ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), null); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
- La petición tiene que ser GET como suele ser habitual en las APIs Rest para realizar consultas.
- La petición devuelve JSON o XML.
- Tiene como parámetros: page (el número de página) y limit (el número de elementos a devolver en cada página).
- La petición devuelve un objeto Response con la entidad PaginatedEntity. Este objeto se crea pasando el número de pagina, el limite de elementos por página.
- El parámetro de clase en la clase PaginatedEntity, es un objeto de la clase RolDtoList, que es un objeto JAXB que contiene internamente un objeto List<RolDto> que es una lista de pojos con la información del rol. La lista de roles, también podría ser del tipo RolDto[]
- Antes de devolver el objeto Response, tenemos que establecer en la instancia de la clase PaginatedEntity, los elementos que devuelve la consulta y el número total de elementos.
Veamos el código de la clase RolDtoList:
package es.um.atica.prueba.services.rest.pojo; import java.util.List; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import org.codehaus.jackson.annotate.JsonUnwrapped; @XmlRootElement(name = "roles") public class RolDtoList { private List<RolDto> roles = null; public RolDtoList() {} public RolDtoList(List<RolDto> roles) { this.roles = roles; } /** * @return the usuarios */ @JsonUnwrapped @XmlElement(name="rol") public List<RolDto> getRoles() { return roles; } /** * @param usuarios the usuarios to set */ public void setRoles( List<RolDto> roles ) { this.roles = roles; } }
Las características de la clase:
- Es un POJO JAXB, tiene la anotación @XmlRootElement, que con la propiedad name establece el nombre del elemento XML.
- Tiene una propiedad roles de tipo List<RolDto>, que es la lista de roles.
- Tiene el método getRoles() con la anotación @XmlElement(name=“rol”) para establecer el nombre del elemento XML y la anotación @JsonUnwrapped que la utilizamos para que el JSON salga de forma igual a la configuración del ejemplo anterior.
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5
Para obtener el resultado en JSON tenemos que añadir en la petición, la cabecera HTTP Accept con valor application/json. Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Si queremos obtener el resultado en XML, tenemos que añadir en la petición, la cabecera HTTP Accept con valor application/xml. Resultado:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <roles> <rol> <descripcion>GESTOR</descripcion> <idRol>GEST</idRol> </rol> <rol> <descripcion>Custom</descripcion> <idRol>CUS</idRol> </rol> <rol> <descripcion>Role ADMIN </descripcion> <idRol>ADM</idRol> </rol> </roles>
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 1 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
Ejemplo cuando devolvemos JSON o XML, con ordenación y sin filtrado
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit, @QueryParam(LinkPagination.SORT_QUERY_PARAM) String sort) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2']", page, limit, sort ); ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(null, pe.getFirstResult(), pe.getLimit(), pe.getProcessedSort(), null); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
- La petición tiene que ser GET como suele ser habitual en las APIs Rest para realizar consultas.
- La petición devuelve JSON o XML.
- Tiene como parámetros: page (el número de página), limit (el número de elementos a devolver en la página), sort (la propiedad para la ordenación).
- La petición devuelve un objeto Response con la entidad PaginatedEntity. Este objeto se crea pasando el número de pagina, el limite de elementos por página.
- Antes de devolver el objeto Response, tenemos que establecer en la instancia de la clase PaginatedEntity, los elementos que devuelve la consulta y el número total de elementos.
- En la llamada al servicio, el campo de ordenación se debe obtener llamando al método getProcessedSort() de la clase PaginatedEntity.
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC
El parámetro sort, indica la ordenación en la consulta, el formato es <nombre_propiedad>:(ASC|asc|DESC|desc). La ordenación puede ser opcional, en ese caso no hace falta el separador usar :. Si queremos tener ordenación múltiple, solo tenemos que separar los pares con ;. El valor del parámetro sort tiene que estar codificado, ya que usa los caracteres especiales : (%3A) y ; (%3B). Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" }, { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" }, { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 1 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5&sort=idRol%3ADESC>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 3 elementos, y el tamaño de la página (limit) es de 5 elementos.
Ejemplo cuando devolvemos JSON o XML, con ordenación y filtrado
Los parámetros para el filtrado, se añaden como parámetros de la petición.
@GET @Path( "/roles" ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML } ) public Response obtenerRolesPaginados(@QueryParam(LinkPagination.PAGE_QUERY_PARAM) Integer page, @QueryParam(LinkPagination.LIMIT_QUERY_PARAM) Integer limit, @QueryParam(LinkPagination.SORT_QUERY_PARAM) String sort, @QueryParam("idRol") String idRol ) { PaginatedEntity<RolDtoList> pe = new PaginatedEntity<RolDtoList>(page, limit, sort); log.info( "Entra en obtenerUsuariosDtoPaginados [page='#0', limit='#1', sort='#2', idRol='#3']", page, limit, sort, idRol ); Map<String, Object> parameters = new HashMap<String,Object>(); if (!Strings.isEmpty( idRol )) { parameters.put( "idRol", idRol ); } ResultQuery<RolDto> datos = servicioRoles.obtenerRolDtoPaginados(parameters, pe.getFirstResult(), pe.getLimit(), pe.getProcessedSort()); pe.setCurrentPage( new RolDtoList( datos.getResultList() ) ); pe.setTotalCount( datos.getResultCount() ); return Response.ok(pe).build(); }
Como podéis ver, la petición tiene las siguientes características:
- La petición tiene que ser GET como suele ser habitual en las APIs Rest para realizar consultas.
- La petición devuelve JSON o XML.
- Tiene como parámetros: page (el número de página), limit (el número de elementos a devolver en la página), sort (la propiedad para la ordenación).
- La petición devuelve un objeto Response con la entidad PaginatedEntity. Este objeto se crea pasando el número de pagina, el limite de elementos por página.
- Antes de devolver el objeto Response, tenemos que establecer en la instancia de la clase PaginatedEntity, los elementos que devuelve la consulta y el número total de elementos.
- En la llamada al servicio, el campo de ordenación se debe obtener llamando al método getProcessedSort() de la clase PaginatedEntity.
- Pasamos los parámetros para filtrar la consulta en un objeto Map<String, Object>, como es habitual.
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A
El parámetro idRol contiene el valor para el filtrado de la consulta. Resultado:
[ { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 1 (el número total de páginas).
- X-Total-Count con valor 1 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=5&idRol%3ADESC&idRol=A>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 5 elementos.
Ejemplo con varias páginas
La petición seria la siguiente:
localhost:8001/prueba/rest/roles?page=1&limit=1
Queremos la página 1 con páginas de un elemento. Resultado:
[ { "idRol": "ADM", "rolPadre": null, "descripcion": "Role ADMIN " } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 3 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=2&limit=1>; rel="next", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Si la petición es la siguiente:
localhost:8001/prueba/rest/roles?page=2&limit=1
Queremos la página 2 con páginas de un elemento. Resultado:
[ { "idRol": "GEST", "rolPadre": null, "descripcion": "GESTOR" } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 3 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="prev", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="next", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Si la petición es la siguiente:
localhost:8001/prueba/rest/roles?page=3&limit=1
Queremos la página 3 con páginas de un elemento. Resultado:
[ { "idRol": "CUS", "rolPadre": null, "descripcion": "Custom" } ]
Las cabeceras de la respuesta HTTP tienen la siguiente información relacionada con la paginación:
- X-Page-Count con valor 3 (el número total de páginas).
- X-Total-Count con valor 3 (el número total de elementos de la consulta).
- Link con valor (HATEOAS para la navegación entre páginas):
<localhost:8001/prueba/rest/roles?page=1&limit=1>; rel="first", <localhost:8001/prueba/rest/roles?page=2&limit=1>; rel="prev", <localhost:8001/prueba/rest/roles?page=3&limit=1>; rel="last"
Estos datos son asi, porque la consulta solo devuelve 1 elementos, y el tamaño de la página (limit) es de 1 elementos.
Resúmen
Los parámetros que tiene que tener la petición, son opcionales, no son obligatorios. Los parámetros principales son:
- page: es el numero de página, sino se pasa o es menor a 1, se le asigna valor 1. Si es mayor al número total de páginas, devuelve una página en blanco, junto a los links de la primera y última página.
- limit: es el numero de elementos por página, sino se pasa o es menor a 1, se le asigna el valor LinkPagination.MAX_RESULTS (25).
- sort: es el parámetro de ordenación. Debe estar codificado y se valida si cumple la expresión regular: ^\w+(\.\w+)*(:(asc|ASC|desc|DESC))?(;\w+(\.\w+)*(:(asc|ASC|desc|DESC))?)*$.
Los parámetros de filtrado son independientes a estos parámetros.
Siempre tenemos que devolver un objeto Response que debe tener como entidad una instancia de PaginatedEntity. Cuando se devuelve SOLO JSON, se puede asignar directamente la lista o array de DTOs a la instancia de PaginatedEntity. Si se devuelve también XML, hay que utilizar POJOs JAXB con la configuración indicada en los ejemplos.
— JUAN MIGUEL BERNAL GONZALEZ 25/06/2020 08:40
- fdw2.0/fundeweb2.0/gt/rest/paginacion.txt
- Última modificación: 19/10/2020 09:52
- por JUAN MIGUEL BERNAL GONZALEZ