0
votes

Main question

In my web app, implemented with Spring/Hibernate/Thymeleaf, I have two domain objects, Owner and Pet. The Pet object has an (optional) Owner field. During an update of a pet through a web form, the user may enter an owner id that exists (in the database) or not.

When the user updates a pet with an existing owner id, the existing owner object is found and associated with the pet. However, when the user updates a pet with an owner id that is not present in the database, the returned pet object (in the "POST" controller method) has a null owner object. In addition, as the owner field within the pet domain could be null, this raises no validation errors.

Ultimately, this may lead to a user submitting erroneous values, thinking that they have added an owner, only for the update of a pet to actually take place with a null owner (if not properly caught).

My question is, what is the recommended way of validating that the id of a nested object exists before update of the parent object is executed.

Further thoughts:

  • I have seen a relevant issue: spring mvc nested model validation, where it is recommended to add a custom validator. In my case, adding a custom validator, that will run within the PetController and validate the Pet/ Owner object will, I think, not work. The returned pet object contains a null owner object, which is a perfectly valid state. I need the validation to happen during data binding. Once we are inside the controller, post-binding, the owner object is null and no validation of the form values can happen.
  • I have done some work on it by adding a custom formatter, from String to Owner. If the owner id cannot be found, this formatter will return a new Owner object with id of "Not Found" (or some numeric/integer equivalent in this case). In my controller, I could then check for an owner with the "erroneous id" and raise an error. However, the default behavior of not returning an object seems safer, as my custom formatter actually returns an invalid object (that may lead to other issues down the line?).

Code

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Pet {

    @Id
    private int id;

    private String name;

    @ManyToOne
    @Valid
    private Owner owner;
    
}

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Owner {

    @Id
    @NotNull
    @NonNull
    private int id;

    private String firstName;

    private String lastName;

}
@Controller
public class PetController {

    @Autowired
    PetRepository petRepository;

    @RequestMapping("editPet")
    @GetMapping
    public String editPet(Model model) {
        Pet pet =  petRepository.getById(1);
        model.addAttribute("pet", pet);
        return "editPet";
    }


    @RequestMapping("updatePet")
    @PostMapping
    public String updatePet(@Validated Pet pet, BindingResult bindingResult) {
        petRepository.save(pet); // if the user submits an invalid owner id, the returned pet will have a null owner 
        // object, but no error raised.
        return "editPet";
    }
}
}
1

1 Answers

0
votes

I have figured out one way of doing the above in a clean way, posting here in case it helps others.

The solution was to utilize Spring's type conversion. The two main points to be gained from the documentation are that firstly, to create a converter we only have to implement the Converter interface. Secondly, to report invalid source values (such as an erroneous owner id in my problem statement above), an IllegalArgumentException should be raised.

Any IllegalArgumentExceptions raised during the conversion will be reported in the BindingResult of the controller method. This means that there is no additional code/effort required to report the illegal value in the controller.

Implementation of the StringToOwner Converter:

First, the repository is checked for an Optional Owner with the given id. If there is no owner with the given id, an exception is raised. There is no need to check for null; as per the documentation, for every call to the converter, the source argument is guaranteed not to be null.

@Component
public class StringToOwner implements Converter<String, Owner> {

    @Autowired
    OwnerRepository ownerRepository;

    @Override
    public Owner convert(String s) {

        Optional<Owner> optOwner = ownerRepository.findById(Integer.parseInt(s));
        if (optOwner.isEmpty()) {
            throw new IllegalArgumentException("Illegal owner id");
        }
        return optOwner.get();
    }
}

Subsequently, we can edit the Controller to check for binding errors:

    @RequestMapping("updatePet")
    @PostMapping
    public String updatePet(@Validated Pet pet, BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute("pet", pet);
            return "editPet"; // Errors can be reported in the generated page
        }
        petRepository.save(pet);
        return "success";
    }