0

Im trying to setup an update/edit page, for an Employee entity that has a oneToMany relation with EmployeeProject entity. Then Project also has a oneToMany relation to EmployeeProject. Saving an employee works fine, you enter a name and contract start/end dates then there is a dynamic list of projects where you use a checkbox to choose the project and add employeeProjectMonths. My problem is in update_employee, when I set the th:field to set the checkbox to show which projects are already set to that employee it uses the EmployeeProject id, not the project id in EmployeeProject, which shows incorrect projects. Im not sure how to do it, ive tried th:field="${employee.employeeProjects.project.id}", also without .id. but I'm not sure how to proceed. So how do I set the th:field="${employee.employeeProjects}" so it chooses the project_id column and not the EmployeeProjects id column?

Heres my entities without constructors, getters and setters.

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @Column(name = "employee_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
    @Column(name = "contracted_from")
    private String contractedFrom;

    @JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
    @Column(name = "contracted_to")
    private String contractedTo;

    @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
    private Set<EmployeeProject> employeeProjects = new HashSet<>();

@Entity
@Table(name = "projects")
public class Project implements Serializable {
    @Id
    @Column(name = "project_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long projectNumber;

    @Column(nullable = false, length = 45)
    private String name;


    @JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
    @Column(name = "start_date", nullable = false)
    private String startDate;

    @JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
    @Column(name = "end_date", nullable = false)
    private String endDate;

@JsonIgnore
    @OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
    private Set<EmployeeProject> employeeProjects = new HashSet<>();

@Entity
@Table(name = "employee_projects")
public class EmployeeProject implements Serializable {

    @Id
    @Column(name = "employee_project_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "employee_id")
    private Employee employee;

    @ManyToOne
    @JoinColumn(name = "project_id")
    private Project project;

    @Column(name = "employee_booked_months")
    private double employeeBookedMonths;

Controller

@GetMapping("/showFormForEmployeeUpdate/{id}")
    public String showFormForUpdate(@PathVariable ( value = "id") long id, Model model) {
        Employee employee = employeeService.getEmployeeById(id);
        List<Project> projects = employeeService.getAllProjects();
        model.addAttribute("employee", employee);
        model.addAttribute("projects", projects);
        return "update_employee";
    }

html form

<form action="#" th:action="@{/ines/saveEmployee}" th:object="${employee}"
            method="POST">
            
            <!-- Add hidden form field to handle update -->
            <input type="hidden" th:field="*{id}" />

            <input type="text" th:field="*{name}"
                   placeholder="Employee Name" class="form-control mb-4 col-4">

            <input type="date" th:field="*{contractedFrom}"
                   placeholder="Contracted From" class="form-control mb-4 col-4">

            <input type="date" th:field="*{contractedTo}"
                   placeholder="Contracted To" class="form-control mb-4 col-4">
            <div th:each="project : ${projects}">
                <div class="form-group blu-margin">
                    <input type="checkbox" th:field="${employee.employeeProjects}" th:name="projectId" th:text="${project.name}" th:value="${project.id}">
                    <input type="text" th:name="employeeProjectMonths" th:value="${employeeProjectMonths}"
                           placeholder="Employee Project Months" class="form-control mb-4 col-4">
                </div>
            </div>
            <button type="submit" class="btn btn-info col-2">Save Employee</button>
        </form>

enter image description here

So the above pic shows the EmployeeProjects table, so for employee with id 1 (employee_id) it checks the box for projects with ids 2, 5 and 7 (wrong column employee_project_id). But I need to check the box for project ids 6, 9 an 7 (project_id).

Thanks in advance

EDIT: So Ive created a dto object, but I still have the same issue. I had to adjust my code recently, instead of just entering the employeeProjectMonths I now enter 2 dates (start and end) then work out the months from that. But this is my new EmployeeProjectDto:

EmployeeProjectDto(id=1, name=John smith, contractedFrom=2022-01-01, contractedTo=2022-12-04, projectList=[ProjectDto(id=1, name=ProjectOne, employeeProjectStartDate=2022-02-01, employeeProjectEndDate=2022-04-30), ProjectDto(id=2, name=ProjectTwo, employeeProjectStartDate=2022-07-01, employeeProjectEndDate=2022-11-30), ProjectDto(id=3, name=ProjectThree, employeeProjectStartDate=2022-08-01, employeeProjectEndDate=2022-09-15)])

The EmployeeProjectDto

@Data
public class EmployeeProjectDto {
    Long id;
    String name;
    private String contractedFrom;
    private String contractedTo;
    List<ProjectDto> projectList;

}

The ProjectDto

@Data
public class ProjectDto {
    private Long id;
    private String name;
    private LocalDate employeeProjectStartDate;
    private LocalDate employeeProjectEndDate;

}

Controller

@GetMapping("/showFormForEmployeeUpdate/{id}")
    public String showFormForUpdate(@PathVariable ( value = "id") long id, Model model) {
        Employee employee = employeeService.getEmployeeById(id);

        EmployeeProjectDto employeeProjectDto = modelMapper.map(employee, EmployeeProjectDto.class);

        //other code

        List<Project> allProjects = employeeService.getAllProjects();
        model.addAttribute("employee", employeeProjectDto);
        model.addAttribute("projects", allProjects);
        return "update_employee";
    }

Here is the html, but the checkboxes and dates are not updated. It crashes and I get a 500 error (below). I know it asks 'Does the return type of the getter match the parameter type of the setter?' Why cant I get the dates in the EmployeeProjectDto, but not in projectList? What am I doing wrong? As a beginner I'm sure its obvious, thanks (and sorry) in advance.

<form action="#" th:action="@{/ines/updateEmployee/{id}(id=${employee.id})}" th:object="${employee}"
            method="POST">

            <!-- Add hidden form field to handle update -->
            <input type="hidden" th:field="*{id}" />

            <input type="text" th:field="*{name}"
                   placeholder="Employee Name" class="form-control mb-4 col-4">

            <input type="date" th:field="*{contractedFrom}"
                   placeholder="Contracted From" class="form-control mb-4 col-4">

            <input type="date" th:field="*{contractedTo}"
                   placeholder="Contracted To" class="form-control mb-4 col-4">
            <div th:each="proj : ${projects}">
                <div class="form-group blu-margin">
                    <input type="checkbox" th:field="*{projectList}" th:name="projectId"
                           th:text="${proj.name}" th:value="${proj.id}">
                    <input type="date"
                           th:field="*{projectList.employeeProjectStartDate}"
                           class="form-control mb-4 col-4">
                    <input type="date"
                           th:field="*{projectList.employeeProjectEndDate}"
                           class="form-control mb-4 col-4">
                </div>
            </div>
            <button type="submit" class="btn btn-info col-2">Save Employee</button>
        </form>

Error

There was an unexpected error (type=Internal Server Error, status=500).
An error happened during template parsing (template: "class path resource [templates/update_employee.html]")
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/update_employee.html]")
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:241)
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parseStandalone(AbstractMarkupTemplateParser.java:100)
    at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:666)
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098)
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072)
    at org.thymeleaf.spring5.view.ThymeleafView.renderFragment(ThymeleafView.java:362)
    at org.thymeleaf.spring5.view.ThymeleafView.render(ThymeleafView.java:189)
    at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1373)
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1118)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: org.attoparser.ParseException: Error during execution of processor 'org.thymeleaf.spring5.processor.SpringInputGeneralFieldTagProcessor' (template: "update_employee" - line 35, col 10)
    at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393)
    at org.attoparser.MarkupParser.parse(MarkupParser.java:257)
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:230)
    ... 48 more
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring5.processor.SpringInputGeneralFieldTagProcessor' (template: "update_employee" - line 35, col 10)
    at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:117)
    at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95)
    at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633)
    at org.thymeleaf.engine.ProcessorTemplateHandler.handleStandaloneElement(ProcessorTemplateHandler.java:918)
    at org.thymeleaf.engine.StandaloneElementTag.beHandled(StandaloneElementTag.java:228)
    at org.thymeleaf.engine.Model.process(Model.java:282)
    at org.thymeleaf.engine.Model.process(Model.java:290)
    at org.thymeleaf.engine.IteratedGatheringModelProcessable.processIterationModel(IteratedGatheringModelProcessable.java:367)
    at org.thymeleaf.engine.IteratedGatheringModelProcessable.process(IteratedGatheringModelProcessable.java:221)
    at org.thymeleaf.engine.ProcessorTemplateHandler.handleCloseElement(ProcessorTemplateHandler.java:1640)
    at org.thymeleaf.engine.TemplateHandlerAdapterMarkupHandler.handleCloseElementEnd(TemplateHandlerAdapterMarkupHandler.java:388)
    at org.thymeleaf.templateparser.markup.InlinedOutputExpressionMarkupHandler$InlineMarkupAdapterPreProcessorHandler.handleCloseElementEnd(InlinedOutputExpressionMarkupHandler.java:322)
    at org.thymeleaf.standard.inline.OutputExpressionInlinePreProcessorHandler.handleCloseElementEnd(OutputExpressionInlinePreProcessorHandler.java:220)
    at org.thymeleaf.templateparser.markup.InlinedOutputExpressionMarkupHandler.handleCloseElementEnd(InlinedOutputExpressionMarkupHandler.java:164)
    at org.attoparser.HtmlElement.handleCloseElementEnd(HtmlElement.java:169)
    at org.attoparser.HtmlMarkupHandler.handleCloseElementEnd(HtmlMarkupHandler.java:412)
    at org.attoparser.MarkupEventProcessorHandler.handleCloseElementEnd(MarkupEventProcessorHandler.java:473)
    at org.attoparser.ParsingElementMarkupUtil.parseCloseElement(ParsingElementMarkupUtil.java:201)
    at org.attoparser.MarkupParser.parseBuffer(MarkupParser.java:725)
    at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:301)
    ... 50 more
Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'projectList.employeeProjectStartDate' of bean class [net.javaguides.springboot.dto.EmployeeProjectDto]: Bean property 'projectList.employeeProjectStartDate' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
    at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:622)
    at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:612)
    at org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:104)
    at org.springframework.validation.AbstractBindingResult.getFieldValue(AbstractBindingResult.java:228)
    at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:129)
    at org.springframework.web.servlet.support.RequestContext.getBindStatus(RequestContext.java:903)
    at org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext.getBindStatus(SpringWebMvcThymeleafRequestContext.java:227)
    at org.thymeleaf.spring5.util.FieldUtils.getBindStatusFromParsedExpression(FieldUtils.java:306)
    at org.thymeleaf.spring5.util.FieldUtils.getBindStatus(FieldUtils.java:253)
    at org.thymeleaf.spring5.util.FieldUtils.getBindStatus(FieldUtils.java:227)
    at org.thymeleaf.spring5.processor.AbstractSpringFieldTagProcessor.doProcess(AbstractSpringFieldTagProcessor.java:174)
    at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74)
    ... 69 more

1 Answers1

1

This issue could pretty easily be resolved by just using DTO objects instead of the standard entities that you are using at the moment.

For example, you could create a DTO for the Employee class that has a list of ID's for projects that they are currently involved with:

List<Long> projectIds;

In order to create the DTO object, you can use something like MapStruct, or create it manually, which ever you prefer.

It would also be smart to use a tag instead of regular check-boxes. They are much simpler to use and can be styled to look just right. Then, you can set the value for the tag by declaring something like:

<select th:field="*{projectIds}" multiple="multiple">
                      <option
                        th:each="project : ${projects}"
                        th:value="${project.id}"
                        th:text="${project.name}"
                      ></option>
 </select>

The object that you send to your POST/PUT controller should now have the ID's for the updated projects.

All that remains is to handle the received object in your service class and you should be good to go!

  • Thanks for your answer, I've added my edit above but Im still missing something. Also I want to stick with the check box and adding the date for now. If you don't mind having another look, would be awesome. – Kayd Anderson Nov 17 '22 at 15:54
  • Hey there, sorry for the late reply, I was quite busy with work for the past couple of days. Based on the stacktrace that you posted, it would seem that you mistakenly tried to connect the field "employeeProjectStartDate" with your project list (the th:field). – Nebojsa Kurilj Nov 24 '22 at 09:02