I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with @PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from @PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
@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); } }
4 Answers
Answers 1
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a @UniqueConstraint
) But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Answers 2
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like @DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local @PathVariable
parameter in the controller method can even be skipped.
Answers 3
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom for validator like this
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"); } } }
This would make sure all the fields are validated and you dont need to pass the id in the form.
Answers 4
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
@RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT) public String saveChannel(@PathVariable Long chanId, @ModelAttribute("channelForm") @Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes) { [...]
0 comments:
Post a Comment