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";
}
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.