title: Spring Boot and Thymeleaf: Object id field is null when sending back to controller
body: <p>I am using Spring MVC with JPA (Hibernate).
  The problem is that when I try to edit a user the id is not sent back (null) and therefore the repository save method makes a new User object instead of updating it.
  I have a User class which has a <code>MappedSuperClass</code> <code>BaseEntity</code>. The mapping is implemented as follows:</p>
  
  <h2>BaseEntity.java</h2>
  
  <pre><code>@MappedSuperclass
  public abstract class BaseEntity {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
      @Version
      private Long version;
  
      protected BaseEntity() {
          id = null;
      }
  
      public Long getId() {
          return id;
      }
  
      public void setId(Long id){
          this.id = id;
      }
  
      public Long getVersion() {
          return version;
      }
  
      public void setVersion(Long version) {
          this.version = version;
      }
  }
  </code></pre>
  
  <h2>User.java</h2>
  
  <pre><code>@Entity
  public class User extends BaseEntity implements UserDetails {
      @NotNull
      @Size(min = 5, max = 30)
      private String name;
      private String username;
      private LocalDate dateOfBirth;
      private String address;
      @JsonIgnore
      private String email;
      @JsonIgnore
      private String password;
      private String barcode;
      @ManyToOne(fetch = FetchType.EAGER)
      @JoinColumn(name = "role_id")
      @JsonIgnore
      private Role role;
      @JoinTable(name = "loan", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "book_id"))
      @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
      private Set&lt;Book&gt; loanedBooks;
  
      private boolean enabled;
  
      public User() {
          super();
      }
  
      public User(String name, String username, String email, String password, Role role, boolean enabled, LocalDate dateOfBirth) {
          this();
          this.name = name;
          this.username = username;
          this.email = email;
          setPassword(password);
          this.role = role;
          this.enabled = enabled;
          this.dateOfBirth = dateOfBirth;
          loanedBooks = new HashSet&lt;&gt;();
  
      }
  
      @Override
      public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
          List&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;&gt;();
          authorities.add(new SimpleGrantedAuthority(role.getName()));
          return authorities;
      }
  
      public void loanBook(Book book) throws BookNotAvailableException{
          if (book.isAvailable()) {
              loanedBooks.add(book);
          } else {
              throw new BookNotAvailableException("Book is not available right now");
          }
  
      }
  
      public void returnBook(Book book) {
          for (Book returnBook : loanedBooks) {
              if (returnBook.getBarcode().equals(book.getBarcode()));
              loanedBooks.remove(returnBook);
          }
      }
  
      public Set&lt;Book&gt; getLoanedBooks() {
          return loanedBooks;
      }
  
      public void setLoanedBooks(Set&lt;Book&gt; loanedBooks) {
          this.loanedBooks = loanedBooks;
      }
  
  
      public void setPassword(String password) {
          this.password = password;
      }
  
      @Override
      public String getPassword() {
          return password;
      }
  
      public String getName() {
          return name;
      }
  
      public void setName(String name) {
          this.name = name;
      }
  
      public String getBarcode() {
          return barcode;
      }
  
      public void setBarcode(String barcode) {
          this.barcode = barcode;
      }
  
      @Override
      public String getUsername() {
          return username;
      }
  
      public void setUsername(String username) {
          this.username = username;
      }
  
      public String getEmail() {
          return email;
      }
  
      public void setEmail(String email) {
          this.email = email;
      }
  
      public Role getRole() {
          return role;
      }
  
      public void setEnabled(boolean enabled) {
          this.enabled = enabled;
      }
  
      public void setRole(Role role) {
          this.role = role;
      }
  
      @Override
      public boolean isAccountNonExpired() {
          return true;
      }
  
      @Override
      public boolean isAccountNonLocked() {
          return true;
      }
  
      @Override
      public boolean isCredentialsNonExpired() {
          return true;
      }
  
      @Override
      public boolean isEnabled() {
          return enabled;
      }
  
      public LocalDate getDateOfBirth() {
          return dateOfBirth;
      }
  
      public void setDateOfBirth(LocalDate dateOfBirth) {
          this.dateOfBirth = dateOfBirth;
      }
  
      public String getAddress() {
          return address;
      }
  
      public void setAddress(String address) {
          this.address = address;
      }
  }
  </code></pre>
  
  <h2>UserController.java</h2>
  
  <pre><code>@Controller
  public class UserController {
      @Autowired
      UserService userService;
  
      @GetMapping(path = "/users")
      @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_LIBRARIAN')")
      public String index(Model model) {
          return "user/index";
      }
  
      // Form for editing an existing user
      @RequestMapping("users/{userId}/edit")
      public String formEditUser(@PathVariable Long userId, Model model) {
          // TODO: Add model attributes needed for new form
          if (!model.containsAttribute("user")) {
              model.addAttribute("user", userService.findById(userId));
          }
          model.addAttribute("action", String.format("/users/%s", userId));
          model.addAttribute("heading", "Edit User");
          model.addAttribute("submit", "Update");
          return "user/form";
      }
  
      // Update an existing user
      @RequestMapping(value = "/users/{userId}", method = RequestMethod.POST)
      public String updateUser(@Valid User user, BindingResult result, RedirectAttributes redirectAttributes) {
          // Update user if valid data was received
          if (result.hasErrors()) {
              // Include validation errors upon redirect
              redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.user", result);
              // Add user if invalid data was received
              redirectAttributes.addFlashAttribute("user", user);
              // Redirect back to the form
              return String.format("redirect:/users/%s/edit", user.getId());
          }
  
          userService.save(user);
  
          redirectAttributes.addFlashAttribute("flash", new FlashMessage("User successfully updated!", FlashMessage.Status.SUCCESS));
  
          //Redirect browser to /users
          return "redirect:/users";
      }
  
      // Form for adding a new user
      @RequestMapping(value = "users/add", method = RequestMethod.GET)
      public String formNewUser(Model model) {
          //..
      }
  
  
      // Add a User
      @RequestMapping(value = "/users", method = RequestMethod.POST)
      public String addUser(@Valid User user, BindingResult result, RedirectAttributes redirectAttributes) {
          // Add user if valid data was received
          if (result.hasErrors()) {
              // Include validation errors upon redirect
              redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.user", result);
              // Add user if invalid data was received
              redirectAttributes.addFlashAttribute("user", user);
              // Redirect back to the form
              return "redirect:/users/add";
          }
          userService.save(user);
  
          redirectAttributes.addFlashAttribute("flash", new FlashMessage("User successfully added!", FlashMessage.Status.SUCCESS));
  
          return "redirect:/users";
      }
  
      // Index for user search and listing
      @GetMapping(path = "/users/search")
      public String userSearch(@RequestParam("name_or_barcode") String nameOrBarcode,Model model) {
          //..
      }
  
  
      // Single user page
      @RequestMapping("/users/{userId}")
      public String user(@PathVariable Long userId, Model model) {
          User user = userService.findById(userId);
          model.addAttribute("user", user);
          return "user/details";
      }
  
      // Delete an existing user
      @RequestMapping(value = "/users/{userId}/delete", method = RequestMethod.POST)
      public String deleteUser(@PathVariable Long userId, RedirectAttributes redirectAttributes) {
          User user = userService.findById(userId);
  
          if(user.getLoanedBooks().size() &gt; 0) {
              redirectAttributes.addFlashAttribute("flash", new FlashMessage("User cannot be deleted because he/she has borrowed books",FlashMessage.Status.FAILURE));
              return String.format("redirect:/users/%s/edit", userId);
          }
          userService.delete(user);
          redirectAttributes.addFlashAttribute("flash", new FlashMessage("User deleted", FlashMessage.Status.SUCCESS));
          return "redirect:/users";
      }
  }
  </code></pre>
  
  <h2>user/form.html</h2>
  
  <p>Note: the hidden id field is there.</p>
  
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;html lang="en" xmlns:th="http://www.thymeleaf.org"&gt;
  &lt;head th:replace="layout :: head"&gt;&lt;/head&gt;
  &lt;body&gt;
  &lt;div th:replace="layout :: nav"&gt;&lt;/div&gt;
  &lt;div th:replace="layout :: flash"&gt;&lt;/div&gt;
  &lt;div class="container"&gt;
      &lt;form th:action="@{${action}}" method="post" th:object="${user}"&gt;
          &lt;input type="hidden" th:field="*{enabled}" /&gt;
          &lt;input type="hidden" th:field="*{role}" /&gt;
          &lt;input type="hidden" th:field="*{id}" /&gt;
          &lt;div class="row"&gt;
              &lt;div class="col s12"&gt;
                  &lt;h2 th:text="${heading}"&gt;New User&lt;/h2&gt;
              &lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="divider"&gt;&lt;/div&gt;
          &lt;div class="row"&gt;
              &lt;div class="col s12 l8" th:classappend="${#fields.hasErrors('name')} ? 'error' : ''"&gt;
                  &lt;input type="text" th:field="*{name}" placeholder="Full Name"/&gt;
                  &lt;div class="error-message" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"&gt;&lt;/div&gt;
              &lt;/div&gt;
          &lt;/div&gt;
  
          &lt;div class="row"&gt;
              &lt;div class="col s12 l8"&gt;
                  &lt;button th:text="${submit}" type="submit" class="button"&gt;Add&lt;/button&gt;
                  &lt;a th:href="@{/users}" class="button"&gt;Cancel&lt;/a&gt;
              &lt;/div&gt;
          &lt;/div&gt;
      &lt;/form&gt;
      &lt;div class="row delete" th:if="${user.id != null}"&gt;
          &lt;div class="col s12 l8"&gt;
              &lt;form th:action="@{|/users/${user.id}/delete|}" method="post"&gt;
                  &lt;button type="submit" class="button"&gt;Delete&lt;/button&gt;
              &lt;/form&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  &lt;div th:replace="layout :: scripts"&gt;&lt;/div&gt;
  &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p>The interesting part is that the enabled field value comes back as 1 which is good.</p>