java - ejemplo - spring rest json
¿Debo poner el ID de mi entidad en la URL o en el formulario como un campo oculto? (4)
Creo que en términos de REST, el ID debe colocarse en la URL, algo como:
https://example.com/module/[ID]
y luego llamo a GET, PUT, DELETE en esa URL. Eso es algo claro, creo. En los controladores Spring MVC, obtendría el ID con @PathVariable. Trabajos.
Ahora, mi problema práctico con Spring MVC es que si hago esto, NO debo incluir la ID como parte del formulario (también), Spring emite advertencias de tipo
Skipping URI variable ''id'' since the request contains a bind value with the same name.
de otra manera. Y también tiene sentido enviarlo solo una vez, ¿verdad? ¿Qué harías si no coinciden?
Eso estaría bien, pero tengo un validador personalizado para mi bean de respaldo de formulario, que necesita saber la ID. (Debe verificar si un determinado nombre único ya se está utilizando para una instancia de entidad diferente, pero no puede hacerlo sin conocer el ID del formulario enviado).
No he encontrado una buena manera de decirle al validador esa ID desde @PathVariable, ya que la validación ocurre incluso antes de que se ejecute el código en mi método de controlador.
¿Cómo resolverías este dilema?
Este es mi controlador (modificado):
@Controller
@RequestMapping("/channels")
@RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
@Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
@RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(@PathVariable Long channelId, @ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
@RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(@PathVariable Long channelId, @ModelAttribute("channelForm") @Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don''t know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That''s fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
@InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
¿No podría simplemente desambiguar los 2 (variables de plantilla URI frente a parámetros) utilizando un nombre diferente para su variable de plantilla URI?
@RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(@PathVariable Long chanId, @ModelAttribute("channelForm") @Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
Creo que finalmente encontré la solución.
¡Pues resulta que Spring también une las variables de ruta para formar beans! No he encontrado esto documentado en ningún lugar, y no lo hubiera esperado, pero cuando intenté cambiar el nombre de la variable de ruta, como sugirió @DavidW (lo cual hubiera esperado que solo tuviera un efecto local en mi método de controlador), me di cuenta que algunas cosas se rompieron, por lo mencionado anteriormente.
Así que, básicamente, la solución es tener la propiedad ID en el objeto de respaldo de formulario, PERO, sin incluir un campo de entrada oculto en el formulario HTML. De esta manera, Spring utilizará la variable de ruta y la rellenará en el formulario. El parámetro local @PathVariable
en el método del controlador puede incluso omitirse.
La forma más limpia de resolver esto, creo, es dejar que la base de datos maneje los duplicados: agregue una restricción única a la columna de la base de datos. (o JPA agregando una @UniqueConstraint
) Pero todavía tiene que capturar la excepción de la base de datos y transformarla en un mensaje fácil de usar.
De esta manera puede mantener el validador de MVC de primavera simple: solo valide los campos, sin necesidad de consultar la base de datos.
Lo que digas es correcto, la forma correcta de diseñar la API de descanso es mencionar el ID de recurso en la variable de ruta. Si miras algunos ejemplos de la jactancia ahora como una API abierta, puedes encontrar ejemplos similares allí.
Para usted, la solución correcta sería utilizar un validador personalizado como este.
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
Esto aseguraría que todos los campos estén validados y que no necesite pasar el ID en el formulario.