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