简体   繁体   中英

Persisting new object with nested objects from Spring MVC view

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;
    }
    // ...
}

As transactions are initiated in the service layer, the only possible choice to implement this synchronization was the

  1. service layer OR
  2. 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());
    }
    // ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM