Spring web mvc + Thymeleaf ModelAttribute 编辑对象中的列表

Spring web mvc + Thymeleaf ModelAttribute editing the list in the object

我有一个用于编辑用户的表单,用户有角色,这是权限类型的对象列表,我希望能够使用复选框(可选)来设置用户将拥有的角色,但我不知道如何在 thymeleaf 中实现表单以及如何将具有给定角色的用户对象传递给控制器​​。

这是我的用户

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name="username", nullable = false, unique = true)
    @NotBlank @Size(min=5, message = "Не менeе 5 знаков")
    private String username;

    @NotBlank @Size(min=5, message = "Не менeе 5 знаков")
    @Column(name = "password")
    private String password;

    @Column(name = "enabled")
    private boolean enabled;

    @Column(name = "name")
    private String name;

    @Column(name = "surname")
    private String surname;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE,
            CascadeType.DETACH,
            CascadeType.REFRESH
    }, fetch = FetchType.EAGER)
    @JoinTable(
            name = "users_authorities",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "authority_id")
    )
    private Set<Authority> authorities = new HashSet<>();

    public User(String username, String password, boolean enabled,
                String name, String surname, String email) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.name = name;
        this.surname = surname;
        this.email = email;
    }

    public void addAuthority(Authority authority) {
        this.authorities.add(authority);
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

}



这是我的编辑用户表单控制器

@GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {

    model.addAttribute("user", userService.findById(id));
    model.addAttribute("allAuthorities", authorityService.findAll());

    return "users/edit-user";
}



这是我的编辑用户表单视图

<body>
<form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">
    <input type="hidden" th:field="*{id}" id="id">

    <label for="username">Username: </label>
    <input type="text" th:field="*{username}" id="username" placeholder="username">
    <br><br>

    <div sec:authorize="hasRole('ROLE_ADMIN')">
        <label for="enabled">Enabled </label>
        <input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
        <br><br>
    </div>

    <label for="name">Name: </label>
    <input type="text" th:field="*{name}" id="name" placeholder="Name">
    <br><br>

    <label for="surname">Surname: </label>
    <input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
    <br><br>

    <label for="email">Email: </label>
    <input type="text" th:field="*{email}" id="email" placeholder="Email">
    <br><br>

    <div th:each="auth:${allAuthorities}">
        <label>
            <span th:text="${auth.authority}"></span>
            <input type="checkbox" name="authorities" th:checked="${user.authorities.contains(auth)}">
        </label>
    </div>

    <input type="submit" value="Edit">
</form>
</body>



它是控制器,它从我的表单中获取数据

@PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
                       @ModelAttribute("user") User user,
                       @RequestParam("authorities") List<Authority> authorities) {
    user.setId(id);
    userService.update(user);

    return "redirect:/admin/users";
}



如果你需要,这是我的权限class

@Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
    @Id
    private Long id;

    @Column(name = "authority")
    private String authority;

    @Transient
    @ManyToMany(mappedBy = "authorities")
    private Set<User> users;

    public Authority(Long id, String authority) {
        this.id = id;
        this.authority = authority;
    }
}

我尝试将角色列表与用户对象分开传递,但这也不起作用,并给出了错误的请求错误。

为了解决这个问题,我在实体权限中添加了一个新的检查字段

@Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
    @Id
    private Long id;

    @Column(name = "authority", nullable = false, unique = true)
    private String authority;

    @Transient
    private boolean checked;

    public Authority(Long id, String authority) {
        this.id = id;
        this.authority = authority;
    }
}

在将视图发送给它之前,我更改了用户对象的 authorites 字段。

@GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {
    User user = userService.findById(id);
    List<Authority> allAuthorities = authorityService.findAll();

    for(Authority auth : allAuthorities) {
        if(user.getAuthorities().contains(auth)) {
            auth.setChecked(true);
        }
    }

    user.setAuthorities(allAuthorities);

    model.addAttribute("user", user);
    return "users/edit-user";
}

我还更改了 thymeleaf 模板
<form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">

    <label for="username">Username: </label>
    <input type="text" th:field="*{username}" id="username" placeholder="username">
    <br><br>

    <div sec:authorize="hasRole('ROLE_ADMIN')">
        <label for="enabled">Enabled </label>
        <input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
        <br><br>
    </div>

    <label for="name">Name: </label>
    <input type="text" th:field="*{name}" id="name" placeholder="Name">
    <br><br>

    <label for="surname">Surname: </label>
    <input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
    <br><br>

    <label for="email">Email: </label>
    <input type="text" th:field="*{email}" id="email" placeholder="Email">
    <br><br>

    <div th:each="auth, itemStat: ${user.authorities}">
        <label>
            <span th:text="${auth.authority}"></span>
            <input type="hidden"
                   th:field="*{authorities[__${itemStat.index}__].id}">
            <input type="hidden"
                   th:field="*{authorities[__${itemStat.index}__].authority}">
            <input type="checkbox" th:checked="${auth.checked}"
                   th:field="*{authorities[__${itemStat.index}__].checked}">
        </label>
    </div>

    <input type="submit" value="Edit">
</form>

我的 put 控制器看起来像这样

@PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
                       @ModelAttribute("user") User user) {
    List<Authority> userAuthorities = user.getAuthorities();
    userAuthorities.removeIf(auth -> !auth.isChecked());

    user.setId(id);
    userService.update(user);

    return "redirect:/admin/users";
}

我在 UserService 中的保存和删除方法如下所示(如果有用的话)
@Override
@Transactional
public User save(User user) {
    Optional<User> userFromDB = userRepository.findByUsername(user.getUsername());
    Optional<Authority> userRole;
    
    if(userFromDB.isPresent()) {
        return null;
    }

    userRole = authorityRepository.findByAuthority("ROLE_USER");
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));

    userRole.ifPresent(user::addAuthority);

    return userRepository.save(user);
}

@Override
@Transactional
public User update(User user) {
    Optional<User> userFromDB = userRepository.findById(user.getId());

    if(userFromDB.isPresent()){
        //Adding the password for successfully update user in DB
        user.setPassword(userFromDB.get().getPassword());
        return userRepository.save(user);
    }

    return null;
}

这可能不是最优雅的解决方案,但我还没有找到另一个解决方案