Issue
Context
There is an application that uses Spring MVC and Spring Data JPA. The JPA provider is Hibernate and the view layer is presented by JSP pages.
There is a page for creating new users. While creating a new user through this page, the user is assigned to a user group (selected from a list). The User
is new but the selected Group
already exists at this point. The User
@Entity
has a set of Group
objects in it. The bidirectional association between User
s and Group
s is @ManyToMany
and a table (group_members
) links these entities.
When the view layer passes a User
object to the controller, the setGroups(Set<Group> groups)
method gets called on the User
. Then this User
object is persisted through userDetailsService.add(user)
which is @Transactional
.
Problem
The Group
s are loaded into the view when the page is rendered and then they become detached from the persistence unit.
While persisting the User
, the associated Group
is detached and the Group
side is not synchronized with the User
.
The User
entity defines utility methods that would synchronize the two sides of the transaction (addGroup(Group group)
and removeGroup(Group group)
) but I cannot find a way to make use of them because this way they can only be called outside the transaction which gets created in the service layer.
Question
What is the best approach to keep all entities in a valid state at all times in this scenario? Should both the User and Group be passed to the service layer?
Code
Some parts were omitted for brevity.
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;
}
// ...
}
Solution
As transactions are initiated in the service layer, the only possible choice to implement this synchronization was the
- service layer OR
- DAO layer
The service layer defines an add(user)
method and a set(user)
method, both of which call the save(user)
method, hence it was logical to sync the association in the DAO layer which includes that method.
Moreover it seems more appropriate to reference the EntityManager
in the DAO layer.
@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());
}
// ...
}
Answered By - Balu