Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>This point is still quite confusing and unclear on the web, so here is the way I solved my problem. This solution is probably not the most optimized one, but it works when <strong>creating and updating</strong> a master entity.</p> <h1>Theory</h1> <ol> <li><p>Use a <code>List</code> instead of a <code>Set</code> for your one-to-many relations which should be dynamically managed.</p></li> <li><p>Initialize your <code>List</code> as an <a href="http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/util/AutoPopulatingList.html" rel="noreferrer"><code>AutoPopulatingList</code></a>. It's a lazy list which allows to <strong>add</strong> dynamically elements.</p></li> <li><p>Add an attribute <code>remove</code> of <code>int</code> to your child entity. This will play the part of a boolean flag and will be usefull when <strong>removing</strong> dynamically an element.</p></li> <li><p>When posting the form, persist only the elements that have the flag <code>remove</code> on <code>0</code> (i.e. <code>false</code>).</p></li> </ol> <h1>Practice</h1> <p>A working full-example: an employer has many employees, an employee has one employer.</p> <h3>Entities:</h3> <p><code>Employer.java</code></p> <pre class="lang-java prettyprint-override"><code>@Entity @Table(name = "employer") public class Employer private Integer id; private String firstname; private String lastname; private String company; private List&lt;Employee&gt; employees; // one-to-many /* getters &amp; setters */ } </code></pre> <p><code>Employee.java</code></p> <pre class="lang-java prettyprint-override"><code>@Entity @Table(name = "employee") public class Employee { private Integer id; @Transient // means "not a DB field" private Integer remove; // boolean flag private String firstname; private String lastname; private Employer employer; // many-to-one /* getters &amp; setters */ } </code></pre> <h3>Controller:</h3> <p><code>EmployerController.java</code></p> <pre class="lang-java prettyprint-override"><code>@Controller @RequestMapping("employer") public class EmployerController { // Manage dynamically added or removed employees private List&lt;Employee&gt; manageEmployees(Employer employer) { // Store the employees which shouldn't be persisted List&lt;Employee&gt; employees2remove = new ArrayList&lt;Employee&gt;(); if (employer.getEmployees() != null) { for (Iterator&lt;Employee&gt; i = employer.getEmployees().iterator(); i.hasNext();) { Employee employee = i.next(); // If the remove flag is true, remove the employee from the list if (employee.getRemove() == 1) { employees2remove.add(employee); i.remove(); // Otherwise, perform the links } else { employee.setEmployer(employer); } } } return employees2remove; } // -- Creating a new employer ---------- @RequestMapping(value = "create", method = RequestMethod.GET) public String create(@ModelAttribute Employer employer, Model model) { // Should init the AutoPopulatingList return create(employer, model, true); } private String create(Employer employer, Model model, boolean init) { if (init) { // Init the AutoPopulatingList employer.setEmployees(new AutoPopulatingList&lt;Employee&gt;(Employee.class)); } model.addAttribute("type", "create"); return "employer/edit"; } @RequestMapping(value = "create", method = RequestMethod.POST) public String create(@Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { // Should not re-init the AutoPopulatingList return create(employer, model, false); } // Call the private method manageEmployees(employer); // Persist the employer employerService.save(employer); return "redirect:employer/show/" + employer.getId(); } // -- Updating an existing employer ---------- @RequestMapping(value = "update/{pk}", method = RequestMethod.GET) public String update(@PathVariable Integer pk, @ModelAttribute Employer employer, Model model) { // Add your own getEmployerById(pk) model.addAttribute("type", "update"); return "employer/edit"; } @RequestMapping(value = "update/{pk}", method = RequestMethod.POST) public String update(@PathVariable Integer pk, @Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) { // Add your own getEmployerById(pk) if (bindingResult.hasErrors()) { return update(pk, employer, model); } List&lt;Employee&gt; employees2remove = manageEmployees(employer); // First, save the employer employerService.update(employer); // Then, delete the previously linked employees which should be now removed for (Employee employee : employees2remove) { if (employee.getId() != null) { employeeService.delete(employee); } } return "redirect:employer/show/" + employer.getId(); } // -- Show an existing employer ---------- @RequestMapping(value = "show/{pk}", method = RequestMethod.GET) public String show(@PathVariable Integer pk, @ModelAttribute Employer employer) { // Add your own getEmployerById(pk) return "employer/show"; } } </code></pre> <h3>View:</h3> <p><code>employer/edit.jsp</code></p> <pre class="lang-html prettyprint-override"><code>&lt;%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %&gt;&lt;%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %&gt;&lt;%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %&gt;&lt;%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %&gt;&lt;!DOCTYPE HTML&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;Edit&lt;/title&gt; &lt;style type="text/css"&gt;.hidden {display: none;}&lt;/style&gt; &lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"&gt;&lt;/script&gt; &lt;script type="text/javascript"&gt; $(function() { // Start indexing at the size of the current list var index = ${fn:length(employer.employees)}; // Add a new Employee $("#add").off("click").on("click", function() { $(this).before(function() { var html = '&lt;div id="employees' + index + '.wrapper" class="hidden"&gt;'; html += '&lt;input type="text" id="employees' + index + '.firstname" name="employees[' + index + '].firstname" /&gt;'; html += '&lt;input type="text" id="employees' + index + '.lastname" name="employees[' + index + '].lastname" /&gt;'; html += '&lt;input type="hidden" id="employees' + index + '.remove" name="employees[' + index + '].remove" value="0" /&gt;'; html += '&lt;a href="#" class="employees.remove" data-index="' + index + '"&gt;remove&lt;/a&gt;'; html += "&lt;/div&gt;"; return html; }); $("#employees" + index + "\\.wrapper").show(); index++; return false; }); // Remove an Employee $("a.employees\\.remove").off("click").on("click", function() { var index2remove = $(this).data("index"); $("#employees" + index2remove + "\\.wrapper").hide(); $("#employees" + index2remove + "\\.remove").val("1"); return false; }); }); &lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;c:choose&gt; &lt;c:when test="${type eq 'create'}"&gt;&lt;c:set var="actionUrl" value="employer/create" /&gt;&lt;/c:when&gt; &lt;c:otherwise&gt;&lt;c:set var="actionUrl" value="employer/update/${employer.id}" /&gt;&lt;/c:otherwise&gt; &lt;/c:choose&gt; &lt;form:form action="${actionUrl}" modelAttribute="employer" method="POST" name="employer"&gt; &lt;form:hidden path="id" /&gt; &lt;table&gt; &lt;tr&gt; &lt;td&gt;&lt;form:label path="firstname"&gt;Firstname&lt;/form:label&gt;&lt;/td&gt; &lt;td&gt;&lt;form:input path="firstname" /&gt;&lt;form:errors path="firstname" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;form:label path="lastname"&gt;Lastname&lt;/form:label&gt;&lt;/td&gt; &lt;td&gt;&lt;form:input path="lastname" /&gt;&lt;form:errors path="lastname" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;form:label path="company"&gt;company&lt;/form:label&gt;&lt;/td&gt; &lt;td&gt;&lt;form:input path="company" /&gt;&lt;form:errors path="company" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Employees&lt;/td&gt; &lt;td&gt; &lt;c:forEach items="${employer.employees}" varStatus="loop"&gt; &lt;!-- Add a wrapping div --&gt; &lt;c:choose&gt; &lt;c:when test="${employer.employees[loop.index].remove eq 1}"&gt; &lt;div id="employees${loop.index}.wrapper" class="hidden"&gt; &lt;/c:when&gt; &lt;c:otherwise&gt; &lt;div id="employees${loop.index}.wrapper"&gt; &lt;/c:otherwise&gt; &lt;/c:choose&gt; &lt;!-- Generate the fields --&gt; &lt;form:input path="employees[${loop.index}].firstname" /&gt; &lt;form:input path="employees[${loop.index}].lastname" /&gt; &lt;!-- Add the remove flag --&gt; &lt;c:choose&gt; &lt;c:when test="${employees[loop.index].remove eq 1}"&gt;&lt;c:set var="hiddenValue" value="1" /&gt;&lt;/c:when&gt; &lt;c:otherwise&gt;&lt;c:set var="hiddenValue" value="0" /&gt;&lt;/c:otherwise&gt; &lt;/c:choose&gt; &lt;form:hidden path="employees[${loop.index}].remove" value="${hiddenValue}" /&gt; &lt;!-- Add a link to remove the Employee --&gt; &lt;a href="#" class="employees.remove" data-index="${loop.index}"&gt;remove&lt;/a&gt; &lt;/div&gt; &lt;/c:forEach&gt; &lt;button id="add" type="button"&gt;add&lt;/button&gt; &lt;/td&gt; &lt;/tr&gt; &lt;/table&gt; &lt;button type="submit"&gt;OK&lt;/button&gt; &lt;/form:form&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>Hope that could help <code>:)</code></p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload