databasespring-bootone-to-manybidispring-thymeleaf

Thymeleaf: saving checkbox and textField data to nested object


I have a basic crud app with 2 entities Employee and Project, they are mapped to a database with a ManyToMany relationship table called employee_projects. I can create projects and employees and also once projects are created they appear as a checkbox in the new/update employee where projects can be selected to that employee. Everything works fine, but... I also want to add employeeProjectMonths as an extra column for the employee_projects table for each project so I researched and found its better to create a OneToMany relationship for the Employee and Project entities then create a EmployeeProjects entity and have a @ManyToOne relating back. When I change from ManyToMany to OneToMany/ManyToOne my app now crashes, if someone could explain why this happens now and how to fix or even a better approach would be great. Ideally it would be great for existing projects to appear as a checkbox in new/edit Employee then if the box is checked a field appears to add the new/updated Employee's employeeBookedMonths. Thanks in advance and sorry if this is blatantly obvious.

Employee

@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;

    @Column(name = "contracted_to")
    private String contractedTo;

    @OneToMany(mappedBy = "employee")
    private Set<EmployeeProject> employeeProjects = new HashSet<>();

/// constructors, getters setters

Project

@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;

    @Column(name = "project_length_months")
    private double projectLengthInMonths;

    @Column(name = "project_booked_months")
    private double currentBookedMonths;

    @Column(name = "remaining_booked_months")
    private double remainingBookedMonths;

    private int numberOfEmployees;

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

///constructors, getters, setters

EmployeeProject

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

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

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

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

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

///constructors, getters, setters

EmployeeController

@Controller
@RequestMapping("/ines")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    
    @GetMapping("/employees")
    public String viewEmployeesPage(Model model) {
        List<Employee> employees = employeeService.getAllEmployees();
        model.addAttribute("listEmployees", employees);
        return "employees.html";
    }
    
    @GetMapping("/showNewEmployeeForm")
    public String showNewEmployeeForm(Model model) {
        Employee employee = new Employee();
        List<Project> projects = employeeService.getAllProjects();
        model.addAttribute("employee", employee);
        model.addAttribute("projects", projects);
        return "new_employee";
    }
    
    @PostMapping("/saveEmployee")
    public String saveEmployee(@ModelAttribute("employee") Employee employee) {
        employeeService.saveEmployee(employee);
        return "redirect:/ines/employees";
    }

***Failure at checkbox line New employee page

<div class="container">
        <h1>Employee Management System</h1>
        <hr>
        <h2>Save Employee</h2>

        <form action="#" th:action="@{/ines/saveEmployee}" th:object="${employee}"
            method="POST">
            <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">
            <th:block th:each="project : ${projects}">
                <div class="form-group blu-margin">
                    <input type="checkbox" th:field="${projects}" th:text="${project.name}" th:value="${project.project_id}">
                </div>
            </th:block>
            <button type="submit" class="btn btn-info col-2">Save Employee</button>
        </form>
        
        <hr>
        
        <a th:href = "@{/ines/employees}"> Back to Employee List</a>
    </div>

Error

Wed Oct 12 10:15:34 CEST 2022
There was an unexpected error (type=Internal Server Error, status=500).
An error happened during template parsing (template: "class path resource [templates/new_employee.html]")
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/new_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: Exception evaluating SpringEL expression: "project.project_id" (template: "new_employee" - line 35, col 78)
    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: Exception evaluating SpringEL expression: "project.project_id" (template: "new_employee" - line 35, col 78)
    at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:290)
    at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166)
    at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66)
    at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109)
    at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138)
    at org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor.doProcess(AbstractStandardExpressionAttributeTagProcessor.java:144)
    at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74)
    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.expression.spel.SpelEvaluationException: EL1008E: Property or field 'project_id' cannot be found on object of type 'net.javaguides.springboot.model.Project' - maybe not public or not valid?
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.access$000(PropertyOrFieldReference.java:51)
    at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:406)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:92)
    at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:112)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:337)
    at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:263)
    ... 75 more

Edit So I managed to work it out using the help below, not sure if this is the best approach but it works.

Form

<form action="#" th:action="@{/ines/saveEmployee}" th:object="${employee}"
            method="POST">
            <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:name="projectId" th:text="${project.name}" th:value="${project.id}">
                    <input type="date" th:name="employeeProjectStartDate" th:value="${employeeProjectStartDate}"
                            class="form-control mb-4 col-4">
                    <input type="date" th:name="employeeProjectEndDate" th:value="${employeeProjectEndDate}"
                           class="form-control mb-4 col-4">
                </div>
            </div>
            <button type="submit" class="btn btn-info col-2">Save Employee</button>
        </form>

Updated controller for saveEmployee

@PostMapping("/saveEmployee")
    public String saveEmployee(@ModelAttribute("employee") Employee employee,
                               @RequestParam("projectId") List<Project> projectIds,
                               @RequestParam("employeeProjectStartDate") List<String> employeeProjectStartDate,
                               @RequestParam("employeeProjectEndDate") List<String> employeeProjectEndDate) {

        List<Double> monthList = employeeService.getListOfEmployeeBookedMonths(employeeProjectStartDate, employeeProjectEndDate);

        employeeService.saveEmployee(employee);
        for (int i=0; i< projectIds.size(); i++) {
            EmployeeProject employeeProject = new EmployeeProject(employee);
            employeeProject.setEmployeeBookedMonths(monthList.get(i));
            employeeProject.setProject(new Project(projectIds.get(i).getId()));
            employeeProject.setEmployeeProjectStartDate(LocalDate.parse(employeeProjectStartDate.get(i)));
            employeeProject.setEmployeeProjectEndDate(LocalDate.parse(employeeProjectEndDate.get(i)));
            employeeProject.setEmployeeProjectName(projectIds.get(i).getName());
            employeeProjectService.saveEmployeeProjectEmployeeOnly(employeeProject);
        }
        return "redirect:/ines/employees";
    }

Solution

  • The compiler has provided you with the information on why the actual Thymeleaf code isn't correct:

    Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'project_id' cannot be found on object of type 'net.javaguides.springboot.model.Project' - maybe not public or not valid?
    

    You have provided a list of Project entities to the model, and the entity has a primary key that you called "id", but you are trying to access a field titled "project_id". What you should do, is create a checkbox for each of the entities within the provided list by iterating through it. The code could be updated by changing:

    <input type="checkbox" th:field="${projects}" th:text="${project.name}" th:value="${project.project_id}">
    

    to

    <input type="checkbox" th:field="${projects}" th:each ="project: ${projects}" th:text="${project.name}" th:value="${project.id}">
    

    or something similar, depending on your implementation. I highly recommend that you check out the Thymeleaf docs page on iterating through object lists.