example - ¿Cómo usar correctamente PagedResourcesAssembler desde Spring Data?
spring tags (2)
Parece que ya has descubierto la forma correcta de usarlo, pero me gustaría profundizar algunos detalles aquí para que otros los encuentren también. Entré a detalles similares sobre PagedResourceAssembler
en esta respuesta .
Modelos de representación
Spring HATEOAS se envía con una variedad de clases base para modelos de representación que facilitan la creación de representaciones equipadas con enlaces. Hay tres tipos de clases proporcionadas de manera inmediata:
-
Resource
: un recurso de elemento. Efectivamente para envolver algún DTO o entidad que captura un solo elemento y lo enriquece con enlaces. -
Resources
: un recurso de recopilación, que puede ser una colección de cosas, pero generalmente son una colección de instancias deResource
. -
PagedResources
: una extensión de losResources
que captura información de paginación adicional como el número total de páginas, etc.
Todas estas clases se derivan de ResourceSupport
, que es un contenedor básico para las instancias de Link
.
Montadores de recursos
Un ResourceAssembler
es ahora el componente atenuante para convertir sus objetos de dominio o DTO en dichas instancias de recursos. La parte importante aquí es que convierte un objeto fuente en un objeto objetivo.
Por lo tanto, PagedResourcesAssembler
tomará una instancia de Spring Data Page
y la transformará en una instancia de PagedResources
al evaluar la Page
y crear los PageMetadata
necesarios, así como los enlaces PageMetadata
y next
para navegar por las páginas. De forma predeterminada, y esta es probablemente la parte más interesante aquí, usará SimplePagedResourceAssembler
(una clase interna de PRA
) para transformar los elementos individuales de la página en instancias de Resource
anidados.
Para permitir personalizar esto, PRA
tiene métodos de toResource(…)
adicionales toResource(…)
que toman un delegado ResourceAssembler
para procesar los elementos individuales. Entonces terminas con algo como esto:
class UserResource extends ResourceSupport { … }
class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource> { … }
Y el código del cliente ahora se ve así:
PagedResourcesAssembler<User> parAssembler = … // obtain via DI
UserResourceAssembler userResourceAssembler = … // obtain via DI
Page<User> users = userRepository.findAll(new PageRequest(0, 10));
// Tell PAR to use the user assembler for individual items.
PagedResources<UserResource> pagedUserResource = parAssembler.toResource(
users, userResourceAssembler);
panorama
A partir del próximo Spring Data Commons 1.7 RC1 (y Spring HATEOAS 0.9 transitivamente), los enlaces prev
y next
se generarán como plantillas de URI compatibles con RFC6540 para exponer los parámetros de solicitud de paginación configurados en HandlerMethodArgumentResolvers
para Pageable
y Sort
.
La configuración que ha mostrado arriba se puede simplificar anotando la clase de configuración con @EnableSpringDataWebSupport
que le permitiría deshacerse de todas las declaraciones de @EnableSpringDataWebSupport
explícitas.
Estoy usando Spring 4.0.0.RELEASE, Spring Data Commons 1.7.0.M1, Spring Hateoas 0.8.0.RELEASE
Mi recurso es un POJO simple:
public class UserResource extends ResourceSupport { ... }
Mi ensamblador de recursos convierte los objetos de usuario en objetos UserResource:
@Component
public class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource> {
public UserResourceAssembler() {
super(UserController.class, UserResource.class);
}
@Override
public UserResource toResource(User entity) {
// map User to UserResource
}
}
Dentro de mi UserController, quiero recuperar la Page<User>
de mi servicio y luego convertirla a PagedResources<UserResource>
usando PagedResourcesAssembler
, como se muestra aquí: https://stackoverflow.com/a/16794740/1321564
@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler assembler) {
Page<User> u = service.get(p)
return assembler.toResource(u);
}
Esto no llama a UserResourceAssembler
y simplemente se devuelve el contenido del User
lugar de mi UserResource
personalizado.
Devolver un solo recurso funciona:
@Autowired
UserResourceAssembler assembler;
@RequestMapping(value="{id}", method=RequestMethod.GET)
UserResource getById(@PathVariable ObjectId id) throws NotFoundException {
return assembler.toResource(service.getById(id));
}
El PagedResourcesAssembler
quiere un argumento genérico, pero luego no puedo usar T toResource(T)
, porque no quiero convertir mi Page<User>
a PagedResources<User>
, especialmente porque User
es un POJO y no hay recursos.
Entonces la pregunta es: ¿Cómo funciona?
EDITAR:
Mi WebMvcConfigurationSupport:
@Configuration
@ComponentScan
@EnableHypermediaSupport
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(pageableResolver());
argumentResolvers.add(sortResolver());
argumentResolvers.add(pagedResourcesAssemblerArgumentResolver());
}
@Bean
public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
return new HateoasPageableHandlerMethodArgumentResolver(sortResolver());
}
@Bean
public HateoasSortHandlerMethodArgumentResolver sortResolver() {
return new HateoasSortHandlerMethodArgumentResolver();
}
@Bean
public PagedResourcesAssembler<?> pagedResourcesAssembler() {
return new PagedResourcesAssembler<Object>(pageableResolver(), null);
}
@Bean
public PagedResourcesAssemblerArgumentResolver pagedResourcesAssemblerArgumentResolver() {
return new PagedResourcesAssemblerArgumentResolver(pageableResolver(), null);
}
/* ... */
}
SOLUCIÓN:
@Autowired
UserResourceAssembler assembler;
@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler pagedAssembler) {
Page<User> u = service.get(p)
return pagedAssembler.toResource(u, assembler);
}
FORMA ALTERNATIVA
Otra forma es usar el encabezado HTTP de rango (leer más en RFC 7233 ). Puede definir el encabezado HTTP de esta manera:
Range: resources=20-41
Eso significa que desea obtener recursos del 20 al 41 (incluido). De esta forma, los consumos de API reciben recursos exactamente definidos.
Es solo una forma alternativa. El rango se usa a menudo con otras unidades (como bytes, etc.)
MANERA RECOMENDADA
Si quieres trabajar con paginación y tener una API realmente aplicable (hipermedia / HATEOAS incluida), entonces te recomiendo añadir página y tamaño de página a tu URL. Ejemplo:
http://host.loc/articles?Page=1&PageSize=20
Luego, puede leer estos datos en su BaseApiController y crear algún objeto QueryFilter en todas sus solicitudes:
{
var requestHelper = new RequestHelper(Request);
int page = requestHelper.GetValueFromQueryString<int>("page");
int pageSize = requestHelper.GetValueFromQueryString<int>("pagesize");
var filter = new QueryFilter
{
Page = page != 0 ? page : DefaultPageNumber,
PageSize = pageSize != 0 ? pageSize : DefaultPageSize
};
return filter;
}
Su API debe devolver alguna colección especial con información sobre la cantidad de elementos.
public class ApiCollection<T>
{
public ApiCollection()
{
Data = new List<T>();
}
public ApiCollection(int? totalItems, int? totalPages)
{
Data = new List<T>();
TotalItems = totalItems;
TotalPages = totalPages;
}
public IEnumerable<T> Data { get; set; }
public int? TotalItems { get; set; }
public int? TotalPages { get; set; }
}
Sus clases modelo pueden heredar alguna clase con soporte de paginación:
public abstract class ApiEntity
{
public List<ApiLink> Links { get; set; }
}
public class ApiLink
{
public ApiLink(string rel, string href)
{
Rel = rel;
Href = href;
}
public string Href { get; set; }
public string Rel { get; set; }
}