使用 Spring MVC 视图中的嵌套对象持久化新对象
Persisting new object with nested objects from Spring MVC view
上下文
有一个应用程序使用 Spring MVC 和 Spring Data JPA。 JPA 提供程序是 Hibernate,视图层由 JSP 页呈现。
有一个用于创建新用户的页面。通过此页面创建新用户时,用户会被分配到一个用户组(从列表中选择)。 User
是新的,但此时所选的 Group
已经存在。 User
@Entity
中有一组 Group
个对象。 User
s 和 Group
s 之间的双向关联是 @ManyToMany
和 table (group_members
) 链接这些实体。
当视图层将 User
对象传递给控制器时,会在 User
上调用 setGroups(Set<Group> groups)
方法。然后这个 User
对象通过 userDetailsService.add(user)
持久化,即 @Transactional
.
问题
Group
在页面呈现时加载到视图中,然后它们与持久性单元分离。
在持久化 User
时,关联的 Group
被分离并且 Group
端未与 User
同步。
User
实体定义了可以同步事务两侧(addGroup(Group group)
和 removeGroup(Group group)
)的实用方法,但我找不到使用它们的方法,因为这样它们只能是在服务层创建的事务外部调用。
问题
在这种情况下,使所有实体始终处于有效状态的最佳方法是什么? User 和 Group 都要传给服务层吗?
代码
为简洁起见,省略了部分内容。
createuser.jsp
<body>
<sf:form id="details" method="post" action="${pageContext.request.contextPath}/docreate" modelAttribute="user">
<h2>Sing up</h2>
<table class="formtable">
<tr>
<td class="label">Userame: </td> <td><sf:input path="username" name="username" type="text" /> <div class="error"> <sf:errors path="username"></sf:errors> </div></td>
</tr>
<tr>
<td class="label">Password: </td> <td><sf:input id="password" path="password" name="password" type="password" /> <div class="error"><sf:errors path="password"></sf:errors> </div></td>
</tr>
<tr>
<td class="label">Confirm password: </td> <td><input id="confirmpass" name="confirmpass" type="password" /> <div id="matchpass"></div></td>
</tr>
<tr>
<td class="label" align="right">Group</td><td><sf:select id="groups" path="groups" items="${groups}" itemValue="id"
itemLabel="groupName"/></td>
</tr>
<tr>
<td> </td> <td class="button"><input value="Create user" type="submit" /> </td>
</tr>
</table>
</sf:form>
</body>
UserController.java
@RequestMapping(value="/docreate", method=RequestMethod.POST)
public String doCreate(Model model, @Validated(FormValidationGroup.class) User user, BindingResult result) {
// User validation ...
user.setEnabled(true);
// Duplicate user check ...
userDetailsService.add(user);
return "usercreated";
}
UserDetailsServiceImpl.java
@Transactional
public void add(User user) {
if (!contains(user.getUsername())) {
userRepository.save(user);
}
}
User.java
@Entity
@Table(name="users",schema="sec")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
private boolean enabled;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name="group_members", schema="sec", joinColumns= { @JoinColumn(name="user_id") }, inverseJoinColumns = { @JoinColumn(name="group_id") } )
private Set<Group> groups = new HashSet<>();
// ...
public void addGroup(Group group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(Group group) {
this.groups.remove(group);
group.getUsers().remove(this);
}
public void setGroups(Set<Group> groups) {
for (Group group : groups) {
this.groups.add(group);
}
}
}
Group.java
@Entity
@Table(name="groups",schema="sec")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String groupName;
// ...
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "groups")
private Set<User> users = new HashSet<>();
// Method to synchronize bidirectional association
public void addUser(User user) {
this.users.add(user);
user.getGroups().add(this);
}
public void removeUser(User user) {
this.users.remove(user);
user.getGroups().remove(this);
}
// ...
public Set<User> getUsers() {
return users != null ? users : new HashSet<>();
}
public void setUsers(Set<User> users) {
this.users = users;
}
// ...
}
由于事务是在服务层启动的,因此实现此同步的唯一可能选择是
- 服务层或
- DAO层
服务层定义了一个add(user)
方法和一个set(user)
方法,这两个方法都调用了save(user)
方法,因此在DAO层同步关联是合乎逻辑的包括那个方法。
而且在DAO层引用EntityManager
似乎更合适。
@Autowired
private GroupRepository groupRepository;
@PersistenceContext
EntityManager em;
// ...
public User save (User user) {
// If the group already exists, then attach it to the persistence context by getting a reference to it else attach it by saving
Set<Group> groups = user.getGroups();
if (!groups.isEmpty()) {
Set<Group> managedGroups = groups.stream()
.map(group -> group.getId() == 0L ? groupRepository.save(group) : em.getReference(Group.class, group.getId()))
.collect(Collectors.toSet());
// Synchronize the bidirectional association
for (Group group : managedGroups) {
user.addGroup(group);
}
}
else {
// Default group
Optional<Group> result = groupRepository.findByGroupName("USERS");
if (result.isPresent())
user.addGroup(result.get());
}
// ...
}
上下文
有一个应用程序使用 Spring MVC 和 Spring Data JPA。 JPA 提供程序是 Hibernate,视图层由 JSP 页呈现。
有一个用于创建新用户的页面。通过此页面创建新用户时,用户会被分配到一个用户组(从列表中选择)。 User
是新的,但此时所选的 Group
已经存在。 User
@Entity
中有一组 Group
个对象。 User
s 和 Group
s 之间的双向关联是 @ManyToMany
和 table (group_members
) 链接这些实体。
当视图层将 User
对象传递给控制器时,会在 User
上调用 setGroups(Set<Group> groups)
方法。然后这个 User
对象通过 userDetailsService.add(user)
持久化,即 @Transactional
.
问题
Group
在页面呈现时加载到视图中,然后它们与持久性单元分离。
在持久化 User
时,关联的 Group
被分离并且 Group
端未与 User
同步。
User
实体定义了可以同步事务两侧(addGroup(Group group)
和 removeGroup(Group group)
)的实用方法,但我找不到使用它们的方法,因为这样它们只能是在服务层创建的事务外部调用。
问题
在这种情况下,使所有实体始终处于有效状态的最佳方法是什么? User 和 Group 都要传给服务层吗?
代码
为简洁起见,省略了部分内容。
createuser.jsp
<body>
<sf:form id="details" method="post" action="${pageContext.request.contextPath}/docreate" modelAttribute="user">
<h2>Sing up</h2>
<table class="formtable">
<tr>
<td class="label">Userame: </td> <td><sf:input path="username" name="username" type="text" /> <div class="error"> <sf:errors path="username"></sf:errors> </div></td>
</tr>
<tr>
<td class="label">Password: </td> <td><sf:input id="password" path="password" name="password" type="password" /> <div class="error"><sf:errors path="password"></sf:errors> </div></td>
</tr>
<tr>
<td class="label">Confirm password: </td> <td><input id="confirmpass" name="confirmpass" type="password" /> <div id="matchpass"></div></td>
</tr>
<tr>
<td class="label" align="right">Group</td><td><sf:select id="groups" path="groups" items="${groups}" itemValue="id"
itemLabel="groupName"/></td>
</tr>
<tr>
<td> </td> <td class="button"><input value="Create user" type="submit" /> </td>
</tr>
</table>
</sf:form>
</body>
UserController.java
@RequestMapping(value="/docreate", method=RequestMethod.POST)
public String doCreate(Model model, @Validated(FormValidationGroup.class) User user, BindingResult result) {
// User validation ...
user.setEnabled(true);
// Duplicate user check ...
userDetailsService.add(user);
return "usercreated";
}
UserDetailsServiceImpl.java
@Transactional
public void add(User user) {
if (!contains(user.getUsername())) {
userRepository.save(user);
}
}
User.java
@Entity
@Table(name="users",schema="sec")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
private boolean enabled;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name="group_members", schema="sec", joinColumns= { @JoinColumn(name="user_id") }, inverseJoinColumns = { @JoinColumn(name="group_id") } )
private Set<Group> groups = new HashSet<>();
// ...
public void addGroup(Group group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(Group group) {
this.groups.remove(group);
group.getUsers().remove(this);
}
public void setGroups(Set<Group> groups) {
for (Group group : groups) {
this.groups.add(group);
}
}
}
Group.java
@Entity
@Table(name="groups",schema="sec")
public class Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String groupName;
// ...
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "groups")
private Set<User> users = new HashSet<>();
// Method to synchronize bidirectional association
public void addUser(User user) {
this.users.add(user);
user.getGroups().add(this);
}
public void removeUser(User user) {
this.users.remove(user);
user.getGroups().remove(this);
}
// ...
public Set<User> getUsers() {
return users != null ? users : new HashSet<>();
}
public void setUsers(Set<User> users) {
this.users = users;
}
// ...
}
由于事务是在服务层启动的,因此实现此同步的唯一可能选择是
- 服务层或
- DAO层
服务层定义了一个add(user)
方法和一个set(user)
方法,这两个方法都调用了save(user)
方法,因此在DAO层同步关联是合乎逻辑的包括那个方法。
而且在DAO层引用EntityManager
似乎更合适。
@Autowired
private GroupRepository groupRepository;
@PersistenceContext
EntityManager em;
// ...
public User save (User user) {
// If the group already exists, then attach it to the persistence context by getting a reference to it else attach it by saving
Set<Group> groups = user.getGroups();
if (!groups.isEmpty()) {
Set<Group> managedGroups = groups.stream()
.map(group -> group.getId() == 0L ? groupRepository.save(group) : em.getReference(Group.class, group.getId()))
.collect(Collectors.toSet());
// Synchronize the bidirectional association
for (Group group : managedGroups) {
user.addGroup(group);
}
}
else {
// Default group
Optional<Group> result = groupRepository.findByGroupName("USERS");
if (result.isPresent())
user.addGroup(result.get());
}
// ...
}