mvc - spring form docs
Habilite la serialización HAL en Spring Boot para el método de controlador personalizado (3)
Estoy tratando de construir una API RESTful con Spring Boot usando spring-boot-starter-data-rest. Hay algunas entidades: cuentas, transacciones, categorías y usuarios, solo lo habitual.
Cuando recupero los objetos en http://localhost:8080/transactions través de la API que se ha generado de forma predeterminada, todo va bien y obtengo una lista con todas las transacciones como objetos JSON como ese:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"_links": {
"self": {
"href": "http://localhost:8080/transactions/5"
},
"category": {
"href": "http://localhost:8080/transactions/5/category"
},
"account": {
"href": "http://localhost:8080/transactions/5/account"
}
}
}
Pero ahora el objetivo es recuperar solo las últimas transacciones bajo esa URL ya que no quiero serializar toda la tabla de la base de datos. Entonces escribí un controlador:
@Controller
public class TransactionController {
private final TransactionRepository transactionRepository;
@Autowired
public TransactionController(TransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
// return the 5 latest transactions
@RequestMapping(value = "/transactions", method = RequestMethod.GET)
public @ResponseBody List<Transaction> getLastTransactions() {
return transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent();
}
}
Cuando ahora intento acceder a http://localhost:8080/transactions hay un
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
debido a la referencia circular entre usuarios y cuentas. Cuando resuelvo esto agregando una anotación @JsonBackReference a la lista de cuentas en Usuario, puedo recuperar la lista de transacciones pero solo con este formato "clásico":
{
"id": 5,
"amount": -4.5,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"account": {
"id": 2,
"name": "Account Tilman",
"owner": {
"id": 1,
"name": "Tilman"
},
"categories": [
{
"id": 1,
"name": "Groceries"
},
{
"id": 2,
"name": "Restaurant"
}
],
"users": [
{
"id": 1,
"name": "Tilman"
}
]
},
"category": {
"id": 2,
"name": "Restaurant"
}
}
Ya no hay enlaces HAL, todo se está serializando directamente por jackson. Traté de agregar
@EnableHypermediaSupport(type = HypermediaType.HAL)
a las clases de entidad, pero eso no me llevó a ninguna parte. Solo quiero que mi controlador devuelva los mismos objetos que la API generada, con enlaces HAL _ en lugar de que cada referencia se serialice. ¿Alguna idea?
EDITAR: OK, después de pensarlo dos veces, me di cuenta de que la anotación @EnableHypermediaSupport debe agregarse a la configuración , por supuesto. Esto resuelve el problema de las referencias circulares y puedo eliminar @JsonBackReference del usuario. Pero solo los atributos del objeto en sí se están serializando, no hay una sección _links:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza"
}
Sé que podría escribir clases de contenedor que extiendan ResourceSupport para todas mis entidades, pero esto parece bastante inútil. Como spring-hateoas puede generar mágicamente las representaciones con la sección _link para la interfaz REST que se crea automáticamente, debería haber una manera de devolver las mismas representaciones desde un controlador personalizado, ¿verdad?
Hay muchos aspectos aquí:
-
Dudo que el recurso de recolección en
/transactions
realmente devuelva una transacción individual como usted describió. Esas representaciones se devuelven para los recursos del artículo. -
Si
TransactionRepository
ya es unPageableAndSortingRepository
el recurso de recopilación se puede ajustar expandiendo la plantilla de URI expuesta en la raíz de la API para el enlace denominadotransactions
. Por defecto, es un parámetro depage
,size
ysort
. Eso significa que los clientes pueden solicitar lo que desea exponer ya. -
Si desea predeterminar las opciones de paginación y clasificación, la implementación de un controlador es la forma correcta. Sin embargo, para lograr una representación como las exposiciones REST de Spring Data, debe devolver al menos instancias de
ResourceSupport
ya que este es el tipo para el que está registrada la asignación HAL.No hay nada mágico aquí si lo piensas. Una entidad simple no tiene enlaces, el
ResourcesSupport
y tipos comoResource<T>
permiten ajustar la entidad y enriquecerla con enlaces como mejor le parezca. Spring Data REST básicamente hace eso por ti usando mucho conocimiento sobre el dominio y la estructura del repositorio que está disponible implícitamente. Puede reutilizar mucho como se muestra a continuación.Hay algunos ayudantes que debe tener en cuenta aquí:
-
PersistentEntityResourceAssembler
, que generalmente se inyecta en el método del controlador. Representa una sola entidad de manera REST de Spring Data, lo que significa que las asociaciones que apuntan a tipos administrados se representarán como enlaces, etc. -
PagedResourcesAssembler
: generalmente se inyecta en la instancia del controlador. Se encarga de preparar los elementos contenidos en la página, opcionalmente usando unResourceAssembler
dedicado.
Lo que básicamente hace Spring Data REST para las páginas es lo siguiente:
PersistentEntityResourceAssembler entityAssembler = …; Resources<?> … = pagedResourcesAssembler.toResources(page, entityAssembler);
Básicamente, se trata de usar
PagedResourcesAssembler
conPersistentEntityResourceAssembler
para representar los elementos.Devolver esa instancia de
Resources
debería darle el diseño de representación que esperaba. -
No necesita crear su propio controlador para limitar los resultados de la consulta u ordenar los resultados. Simplemente cree un método de consulta en su repositorio:
public interface TransactionRepository extends MongoRepository<Transaction, String> {
List<Transaction> findFirst10ByOrderByDateDesc();
}
Spring Data REST lo exportará automáticamente como un
recurso de método
en
/transactions/search/findFirst10ByOrderByDateDesc
.
Para usar PersistentEntityResourceAssembler en el controlador, debemos marcarlo como @RepositoryRestController
@RestController
@RequestMapping("/categories")
@RepositoryRestController
public class CategoryController implements ValidableController {
// dependencies
@RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PersistentEntityResource> create(@Valid @RequestBody CategoryForm category,
BindingResult validation,
PersistentEntityResourceAssembler resourceAssembler)
{
validate(validation);
Category entity = categoryConverter.convert(category);
entity = categoryService.save(entity);
return ResponseEntity.ok(resourceAssembler.toFullResource(entity));
}
Construye una respuesta de estilo HAL bastante agradable
{
"createdTime": "2018-07-24T00:55:32.854",
"updatedTime": "2018-07-24T00:55:32.855",
"name": "cfvfcdfgdfdfdfs32",
"options": [
"aaa",
"bbb"
],
"_links": {
"self": {
"href": "http://localhost:8080/shop/categories/34"
},
"category": {
"href": "http://localhost:8080/shop/categories/34{?projection}",
"templated": true
},
"products": {
"href": "http://localhost:8080/shop/categories/34/products"
},
"categories": {
"href": "http://localhost:8080/shop/categories/34/categories{?projection}",
"templated": true
},
"parent": {
"href": "http://localhost:8080/shop/categories/34/parent{?projection}",
"templated": true
}
}
}