Search code examples
javaspringhibernatespring-mvcthymeleaf

Problem with passing objects from web form to Spring controller


I'm stuck with such problem: How can I pass an object from the web form to the Spring controller? (I'm also using Hibernate and Thymeleaf)

I made everything like it is done here: POSTing data with many to one relationship using Thymeleaf but it's not working for me. I'm getting such error:

Field error in object 'car' on field 'engine': rejected value [1]; codes [typeMismatch.car.engine,typeMismatch.engine,typeMismatch.ua.klieshchunov.spring.memorial.model.entity.Engine,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [car.engine,engine]; arguments []; default message [engine]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'ua.klieshchunov.spring.memorial.model.entity.Engine' for property 'engine'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'ua.klieshchunov.spring.memorial.model.entity.Engine' for property 'engine': no matching editors or conversion strategy found]

I understand what does this error mean. It is trying to put the value of selected option into the field of Car object with type Engine. But I do not understand, how to make it work :P

Here is all the code which can be related to this problem

Car

@Entity
@Table(name="cars")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="car_id")
    private int id;

    @Column(name="car_model")
    @Size(min=1, max=50, message="Model should be between 1 and 50 characters")
    private String model;

    @Column(name="car_price")
    @Min(value=0, message="The value must me positive")
    private int price;

    @ManyToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name="engine_id")
    private Engine engine;
}

Engine

@Entity
@Table(name="engines")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Engine {
    @Id
    @Column(name="engine_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name="model")
    @Size(min=1, max=50, message="Model should be between 1 and 50 characters")
    private String model;
}

CarController (Controller)

 @GetMapping("/new")
    public String newCar(@ModelAttribute("car") Car car,
                         Model model) {
        model.addAttribute("engines", engineDAO.retrieve());
        return "/car/new";
    }

    @PostMapping("/save")
    public String create(@Valid @ModelAttribute("car") Car car,
                         BindingResult bindingResult,
                         Model model) {
        if (bindingResult.hasErrors()) {
            for (ObjectError objectError : bindingResult.getAllErrors()) {
                System.out.println(objectError);
            }
            model.addAttribute("engines", engineDAO.retrieve());
            return "/car/new";
        }
        carDAO.create(car);
        return "/car/index";
    }

new.html (View)

...

<form th:method="POST" th:action="@{/car/save}" th:object="${car}">
    ...
    <label for="engine">Choose engine: </label>
    <select th:field="*{engine}" id="engine">
        <option th:each="dropDownItem : ${engines}"
                th:value="${dropDownItem.id}"
                th:text="${dropDownItem.model}" >
        </option>
    </select>

    <br/>
    <input type="submit" value="Create">

</form>
...

Solution

  • All I needed to do is to change th:field value of the select block from "*{engine}" to "*{engine.id}" in form.

    new.html

    ...
    <select th:field="*{engine.id}" id="engine">
        
        <option th:each="dropDownItem : ${engines}"
                th:value="${dropDownItem.id}"
                th:text="${dropDownItem.model}" >
        </option>
    
    </select>
    ...
    

    And after that in controller I find the desired record in the db (engine with chosen id) and set filled Engine object to my Сar object, all other entries of which I already got from the form

    CarController

    @PostMapping("/save")
    public String create(@Valid @ModelAttribute("car") Car car,
                         BindingResult bindingResult,
                         Model model) {
        if (bindingResult.hasErrors()) {
            for (ObjectError objectError : bindingResult.getAllErrors()) {
                System.out.println(objectError);
            }
            model.addAttribute("engines", engineDAO.retrieve());
            return "/car/new";
        }
        car.setEngine(engineDAO.read(car.getEngine().getId()));
        carDAO.create(car);
        return "redirect:/car";
    }
    

    Ye, coding at night wasn't really a superb idea xD