Предисловие
У меня есть приложение для весенней загрузки с сущностью User с набором Role.
В шаблоне редактирования пользователя я показываю роли пользователя с помощью <select>multiple. При отображении вида существующего User с его набором Role я пытаюсь пометить как выбранные только роли в наборе.
Thymeleaf предоставляет для этого два инструмента:
th:selected: ожидает логического значения (если выбрано true)
#sets: который предоставляет несколько полезных методов, подобных java.util.Set, в данном случае используется contains().
Эта проблема
При добавлении в модель найденного User и всех возможных Role в форме HashSet, использование #sets.contains() всегда возвращает false при использовании найденных ролей пользователя и всех ролей в качестве параметров, поэтому не выбирает роли пользователя при загрузке формы.
Если я использую нотацию th:selected = "${{user.roles}}", будут выбраны все параметры (даже те, которых нет у пользователя).
Код
Пользователь
public class User
{
private Long id;
private String username;
private String password;
private String passwordConfirm;
private Set<Role> roles;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getUsername()
{
return username;
}
public void setUsername(String username)
{
this.username = username;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
@Transient
public String getPasswordConfirm()
{
return passwordConfirm;
}
public void setPasswordConfirm(String passwordConfirm)
{
this.passwordConfirm = passwordConfirm;
}
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_role", joinColumns = @JoinColumn(name = "users_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
public Set<Role> getRoles()
{
return roles;
}
public void setRoles(Set<Role> roles)
{
this.roles = roles;
}
}
Роль
public class Role
{
private Long id;
private String name;
private Set<User> users;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
@ManyToMany(mappedBy = "roles")
public Set<User> getUsers()
{
return users;
}
public void setUsers(Set<User> users)
{
this.users = users;
}
}
Контроллер
@Controller
@RequestMapping("/admin")
public class AdminController
{
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@RequestMapping("/user/edit/{id}")
public String editUser(Model model, @PathVariable("id") long id)
{
User user = userService.findByUserId(id);
HashSet<Role> foundRoles = roleService.getAllRoles();
model.addAttribute("user", user);
model.addAttribute("userRoles", foundRoles);
return "admin/adminUserDetail";
}
}
Форма
<form role = "form" th:action = "@{/registration}" method = "POST"
th:object = "${user}">
<div th:if = "${#fields.hasErrors('*')}">
<div class = "alert alert-danger" role = "alert">
<h3 class = "alert-heading">It seems we have a couple problems with your input</h3>
<li th:each = "err : ${#fields.errors('*')}" th:text = "${err}"></li>
</div>
</div>
<div class = "form-group">
<label>Username: </label> <input class = "form-control" type = "text" th:field = "${user.username}"
placeholder = "Username" name = "username"/>
<label>Password: </label> <input class = "form-control" type = "password" th:field = "${user.password}"
placeholder = "Password" name = "password"/>
<label>Password Confirm: </label> <input type = "password"
th:field = "${user.passwordConfirm}" class = "form-control"
placeholder = "Password Confirm"/>
<select class = "form-control" multiple = "multiple">
<option th:each = "role : ${userRoles}"
th:value = "${role.id}"
th:selected = "${#sets.contains(user.roles, role)}"
th:text = "${role.name}">Role name
</option>
</select>
<button type = "submit" class = "btn btn-success">Update</button>
</div>
</form>Это правильный ответ. Я использую HashSet для своей реализации Set, поэтому он выполняет сравнение объектов при вызове contains (), что требует явного метода сравнения задействованных объектов. В этом сравнении используются методы объектов как equals, так и hashcode.
При использовании Set находящийся там объект должен реализовывать как hashCode, так и equals, поскольку это используется для определения того, находится ли объект уже в Set. Если только это не SortedSet, который использует либо Comparator, либо естественный порядок, выраженный через ваш объект, реализующий Comparable.
Поскольку вы этого не сделаете, ни один из тех, кто использует contains, просто всегда будет возвращать false даже для, казалось бы, одного и того же экземпляра Role. Потому что по контракту их нет.
Чтобы исправить, внедрите метод equals и hashCode в объект User и Role.
public class Role {
public int hashCode() {
return Objects.hash(this.name);
}
public boolean equals(Object o) {
if (o == this) { return true; }
if (o == null || !(o instanceof Role) ) { return false; }
return Objects.equals(this.name, ((Role) o).name);
}
}
Что-то в этом роде должно помочь.
Ни ваш
User, ни вашRoleне имеют реализацииhashCode. поэтому каждый новыйRoleбудет иметь свой хэш-код. В зависимости от типа набора используется хэш-код или равно, чтобы определить, находится ли там объект. У вас нет ни того, ни другого, поэтому он всегда будет возвращать false.