百里香叶装订系列
thymeleaf binding collections
我在使用 spring 和 thymeleaf 绑定集合时遇到问题。每次发送表单时,我的对象集合都设置为 null (User.postions),我的示例如下:
我的控制器:
@RequestMapping(value = urlFragment + "/add", method = RequestMethod.GET)
public String addPosition(Model model) {
HashSet<Position> positions = new HashSet<Position>(positionRepository.findByEnabledTrueOrderByNameAsc());
User employee = new User();
for (Position position : positions) {
employee.addPosition(position);
}
model.addAttribute("employee", employee);
return "crud/employee/add";
}
@RequestMapping(value = urlFragment + "/add", method = RequestMethod.POST)
public String processNewEmployee(Model model, @Valid @ModelAttribute("employee") User employee, BindingResult result) {
String templatePath = "crud/employee/add";
if (!result.hasErrors()) {
userRepository.save(employee);
model.addAttribute("success", true);
}
return templatePath;
}
我的员工表格:
<form action="#" th:action="@{/panel/employee/add}" th:object="${employee}" method="post">
<div class="row">
<div class="col-md-6">
<label th:text="#{first_name}">First name</label>
<input class="form-control" type="text" th:field="*{userProfile.firstName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{last_name}">Last name</label>
<input class="form-control" type="text" th:field="*{userProfile.lastName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{email}">Email</label>
<input class="form-control" type="text" th:field="*{email}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="checkbox">
<button type="submit" class="btn btn-success" th:text="#{add_employee}">
Add employee
</button>
</div>
</div>
</div>
</form>
用户实体:
@Entity
@Table(name="`user`")
public class User extends BaseModel {
@Column(unique = true, nullable = false, length = 45)
private String email;
@Column(nullable = false, length = 60)
private String password;
@Column
private String name;
@Column
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {@JoinColumn(name = "role_id", nullable = false)}
)
private Collection<Role> roles = new HashSet<Role>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "user_position",
joinColumns = {@JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {@JoinColumn(name = "position_id", nullable = false)}
)
private Collection<Position> positions = new HashSet<Position>();
public User() {
}
public User(String email, String password, boolean enabled) {
this.email = email;
this.password = password;
this.enabled = enabled;
}
public User(String email, String password, boolean enabled, Set<Role> roles) {
this.email = email;
this.password = password;
this.enabled = enabled;
this.roles = roles;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<Position> getPositions() {
return positions;
}
private void setPositions(Collection<Position> positions) {
this.positions = positions;
}
public boolean addPosition(Position position) {
return positions.add(position);
}
public boolean removePosition(Position position) {
return positions.remove(position);
}
public Collection<Role> getRoles() {
return roles;
}
private void setRoles(Collection<Role> roles) {
this.roles = roles;
}
public boolean addRole(Role role) {
return roles.add(role);
}
public boolean removeRole(Role role) {
return roles.remove(role);
}
@Override
public String toString() {
return User.class + " - id: " + getId().toString() + ", email: " + getEmail();
}
}
我在某处读到我必须创建 equals() 和 hashCode(),所以我在我的 Position Entity 中做了。
public boolean equals(Position position) {
return this.getId() == position.getId();
}
public int hashCode(){
return this.getId().hashCode() ;
}
以下是 post 方法发送的数据:
这是我的结果:
我的spring版本:4.1.6.RELEASE
thymeleaf-spring4 版本:2.1.4.RELEASE
thymeleaf-layout-dialect 版本:1.2.8
我当然希望 positions 是具有 id = 2 的对象 Position 的一个元素的 HashCode。
你可以帮帮我吗?我做错了什么?
这是因为您使用 ${position.id}
作为您的期权价值。这意味着 spring 无法计算出值中使用的 id 与实际 Position 对象之间的关系。只尝试 ${position}
作为你的价值,它应该有效:
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position}"
th:text="${position.name}">Wireframe
</option>
</select>
(确保您已在您的职位上实施 hashCode 和 equals class)
如果这仍然不起作用,您可能必须为 Position 实现 Formatter,以明确转换。请参阅此示例 thymeleafexamples-selectmultiple。
我遇到了类似的问题,我通过添加 Formatter class 并将 Formatter 添加到 MVC 的配置中解决了这个问题:
@Override
protected void addFormatters(FormatterRegistry registry){
registry.addFormatter(new PositionFormater());
...
}
和位置 class 格式化程序应该看起来像这样:
位置格式器:
public class PositionFormatter implements Formatter<Position>{
/** String representing null. */
private static final String NULL_REPRESENTATION = "null";
@Resource
private PositionRepository positionRepository;
public PositionFormatter() {
super();
}
@Override
public String print(Position position, Locale locale) {
if(position.equals(NULL_REPRESENTATION)){
return null;
}
try {
Position newPosition = new Position();
newPosition.setId(position.getId());
return newPosition.getId().toString();
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + position + "` to a valid id");
}
}
@Override
public Position parse(String text, Locale locale) throws ParseException {
if (text.equals(NULL_REPRESENTATION)) {
return null;
}
try {
Long id = Long.parseLong(text);
Position position = new Position();
position.setId(id);
return position;
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + text + "` to valid Position");
}
}
}
就我而言,这两个解决了所有问题。我有几个格式化程序,我所做的就是制作一个并将其添加到配置文件(在我的例子中是 WebMVCConfig)
Check my original post where I resolved this problem
感谢大家回答我的问题。你帮了我很多。不幸的是,我不得不在一件事上不同意你的看法。您向我展示了以下示例:
newPosition.setId(position.getId());
相同的示例在 Andrew github 存储库中。我认为使用 setId() 方法是不好的做法。因此,我将展示我的解决方案,并在将其标记为答案之前等待一些评论。
WebMvcConfig Class
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.smartintranet")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@PersistenceContext
private EntityManager entityManager;
// (....rest of the methods.......)
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
formatterRegistry.addFormatter(new PositionFormatter(entityManager));
}
}
位置格式化程序class
public class PositionFormatter implements Formatter<Position> {
private EntityManager entityManager;
public PositionFormatter(EntityManager entityManager) {
this.entityManager = entityManager;
}
public String print(Position position, Locale locale) {
if(position.getId() == null){
return "";
}
return position.getId().toString();
}
public Position parse(String id, Locale locale) throws ParseException {
return entityManager.getReference(Position.class, Long.parseLong(id));
}
}
employeeForm.html
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{position}" class="form-control">
<option th:each="position : ${allPositions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
最后一个,EmployeeController Class
@Controller
public class EmployeeController extends AbstractCrudController {
// (...rest of dependency and methods....)
@Transactional
@RequestMapping(value = urlFragment + "/create", method = RequestMethod.GET)
public String createNewEmployee(Model model) {
prepareEmployeeForm(model);
return "crud/employee/create";
}
@Transactional
@RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, @ModelAttribute("employee") Employee employee, BindingResult result) {
if (!result.hasErrors()) {
// Look here it is important line!
entityManager.merge(employee.getUser());
}
prepareEmployeeForm(model);
return "crud/employee/create";
}
}
这是我的解决方案。这里有什么不好?我认为那一行:
entityManager.merge(employee.getUser());
我不能在这里使用:
userRepository.save(employee.getUser());
因为 Position 实体是分离的,当我使用保存方法时它在这种情况下运行 em.persist()
所以我 运行 手动 em.merge()
。我知道这段代码并不完美,但我认为这个解决方案比使用 setId() 更好。我将感谢建设性的批评。
我在使用 spring 和 thymeleaf 绑定集合时遇到问题。每次发送表单时,我的对象集合都设置为 null (User.postions),我的示例如下:
我的控制器:
@RequestMapping(value = urlFragment + "/add", method = RequestMethod.GET)
public String addPosition(Model model) {
HashSet<Position> positions = new HashSet<Position>(positionRepository.findByEnabledTrueOrderByNameAsc());
User employee = new User();
for (Position position : positions) {
employee.addPosition(position);
}
model.addAttribute("employee", employee);
return "crud/employee/add";
}
@RequestMapping(value = urlFragment + "/add", method = RequestMethod.POST)
public String processNewEmployee(Model model, @Valid @ModelAttribute("employee") User employee, BindingResult result) {
String templatePath = "crud/employee/add";
if (!result.hasErrors()) {
userRepository.save(employee);
model.addAttribute("success", true);
}
return templatePath;
}
我的员工表格:
<form action="#" th:action="@{/panel/employee/add}" th:object="${employee}" method="post">
<div class="row">
<div class="col-md-6">
<label th:text="#{first_name}">First name</label>
<input class="form-control" type="text" th:field="*{userProfile.firstName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{last_name}">Last name</label>
<input class="form-control" type="text" th:field="*{userProfile.lastName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{email}">Email</label>
<input class="form-control" type="text" th:field="*{email}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="checkbox">
<button type="submit" class="btn btn-success" th:text="#{add_employee}">
Add employee
</button>
</div>
</div>
</div>
</form>
用户实体:
@Entity
@Table(name="`user`")
public class User extends BaseModel {
@Column(unique = true, nullable = false, length = 45)
private String email;
@Column(nullable = false, length = 60)
private String password;
@Column
private String name;
@Column
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {@JoinColumn(name = "role_id", nullable = false)}
)
private Collection<Role> roles = new HashSet<Role>();
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "user_position",
joinColumns = {@JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {@JoinColumn(name = "position_id", nullable = false)}
)
private Collection<Position> positions = new HashSet<Position>();
public User() {
}
public User(String email, String password, boolean enabled) {
this.email = email;
this.password = password;
this.enabled = enabled;
}
public User(String email, String password, boolean enabled, Set<Role> roles) {
this.email = email;
this.password = password;
this.enabled = enabled;
this.roles = roles;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<Position> getPositions() {
return positions;
}
private void setPositions(Collection<Position> positions) {
this.positions = positions;
}
public boolean addPosition(Position position) {
return positions.add(position);
}
public boolean removePosition(Position position) {
return positions.remove(position);
}
public Collection<Role> getRoles() {
return roles;
}
private void setRoles(Collection<Role> roles) {
this.roles = roles;
}
public boolean addRole(Role role) {
return roles.add(role);
}
public boolean removeRole(Role role) {
return roles.remove(role);
}
@Override
public String toString() {
return User.class + " - id: " + getId().toString() + ", email: " + getEmail();
}
}
我在某处读到我必须创建 equals() 和 hashCode(),所以我在我的 Position Entity 中做了。
public boolean equals(Position position) {
return this.getId() == position.getId();
}
public int hashCode(){
return this.getId().hashCode() ;
}
以下是 post 方法发送的数据:
这是我的结果:
我的spring版本:4.1.6.RELEASE thymeleaf-spring4 版本:2.1.4.RELEASE thymeleaf-layout-dialect 版本:1.2.8
我当然希望 positions 是具有 id = 2 的对象 Position 的一个元素的 HashCode。 你可以帮帮我吗?我做错了什么?
这是因为您使用 ${position.id}
作为您的期权价值。这意味着 spring 无法计算出值中使用的 id 与实际 Position 对象之间的关系。只尝试 ${position}
作为你的价值,它应该有效:
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position}"
th:text="${position.name}">Wireframe
</option>
</select>
(确保您已在您的职位上实施 hashCode 和 equals class)
如果这仍然不起作用,您可能必须为 Position 实现 Formatter,以明确转换。请参阅此示例 thymeleafexamples-selectmultiple。
我遇到了类似的问题,我通过添加 Formatter class 并将 Formatter 添加到 MVC 的配置中解决了这个问题:
@Override
protected void addFormatters(FormatterRegistry registry){
registry.addFormatter(new PositionFormater());
...
}
和位置 class 格式化程序应该看起来像这样:
位置格式器:
public class PositionFormatter implements Formatter<Position>{
/** String representing null. */
private static final String NULL_REPRESENTATION = "null";
@Resource
private PositionRepository positionRepository;
public PositionFormatter() {
super();
}
@Override
public String print(Position position, Locale locale) {
if(position.equals(NULL_REPRESENTATION)){
return null;
}
try {
Position newPosition = new Position();
newPosition.setId(position.getId());
return newPosition.getId().toString();
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + position + "` to a valid id");
}
}
@Override
public Position parse(String text, Locale locale) throws ParseException {
if (text.equals(NULL_REPRESENTATION)) {
return null;
}
try {
Long id = Long.parseLong(text);
Position position = new Position();
position.setId(id);
return position;
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + text + "` to valid Position");
}
}
}
就我而言,这两个解决了所有问题。我有几个格式化程序,我所做的就是制作一个并将其添加到配置文件(在我的例子中是 WebMVCConfig)
Check my original post where I resolved this problem
感谢大家回答我的问题。你帮了我很多。不幸的是,我不得不在一件事上不同意你的看法。您向我展示了以下示例:
newPosition.setId(position.getId());
相同的示例在 Andrew github 存储库中。我认为使用 setId() 方法是不好的做法。因此,我将展示我的解决方案,并在将其标记为答案之前等待一些评论。
WebMvcConfig Class
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.smartintranet")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@PersistenceContext
private EntityManager entityManager;
// (....rest of the methods.......)
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
formatterRegistry.addFormatter(new PositionFormatter(entityManager));
}
}
位置格式化程序class
public class PositionFormatter implements Formatter<Position> {
private EntityManager entityManager;
public PositionFormatter(EntityManager entityManager) {
this.entityManager = entityManager;
}
public String print(Position position, Locale locale) {
if(position.getId() == null){
return "";
}
return position.getId().toString();
}
public Position parse(String id, Locale locale) throws ParseException {
return entityManager.getReference(Position.class, Long.parseLong(id));
}
}
employeeForm.html
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{position}" class="form-control">
<option th:each="position : ${allPositions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
最后一个,EmployeeController Class
@Controller
public class EmployeeController extends AbstractCrudController {
// (...rest of dependency and methods....)
@Transactional
@RequestMapping(value = urlFragment + "/create", method = RequestMethod.GET)
public String createNewEmployee(Model model) {
prepareEmployeeForm(model);
return "crud/employee/create";
}
@Transactional
@RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, @ModelAttribute("employee") Employee employee, BindingResult result) {
if (!result.hasErrors()) {
// Look here it is important line!
entityManager.merge(employee.getUser());
}
prepareEmployeeForm(model);
return "crud/employee/create";
}
}
这是我的解决方案。这里有什么不好?我认为那一行:
entityManager.merge(employee.getUser());
我不能在这里使用:
userRepository.save(employee.getUser());
因为 Position 实体是分离的,当我使用保存方法时它在这种情况下运行 em.persist()
所以我 运行 手动 em.merge()
。我知道这段代码并不完美,但我认为这个解决方案比使用 setId() 更好。我将感谢建设性的批评。