JSF selectCheckboxMenu & Hibernate @ManyToMany remove values

429 views Asked by At

First of all some details von the used software: I'm working with primefaces 6.2.11 on Wildfly 14.0.1Final with JSF 2.3.

Problemdescription: I have two entities (User & Group) related many2many and I want to remove a relation on the side which is not the owner (the entity with the mappedBy). I cant change the owner sides because this is legacy code. I know that I have to remove the relation on both ends but my code does not work. Adding values to the list is no problem but removing them is not possible. I tried this several days ago with Wildfly 10 and it was okay but not with the new one.

One problem is that the primefaces list does no notify me via ajax event when an item will be removed. If this would be the case I could handle the remove from my group list and user list in the group. Because I dont have an event like that I tried to handle this in my service which tries to save the user.

I have two entities:

@Entity
@Table(name = "USER")
public class User implements Serializable {
    private static final long serialVersionUID = -1699265057144017750L;

    @Column(name = "ID", nullable = false)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "USER_NAME", length = 45, unique = true)
    @Basic
    @NotNull
    private String userName;

    @ManyToMany(fetch = FetchType.LAZY, targetEntity = Group.class, mappedBy = "userList")
    @OrderBy("groupName")
    private Set<Group> groupList;

    // getter setter ...
}

@Entity
@Table(name = "GROUP")
public class User implements Serializable {
    private static final long serialVersionUID = -3403518747362744630L;

    @Column(name = "ID", nullable = false)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @ManyToMany(fetch = FetchType.LAZY, targetEntity = User.class, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @JoinTable(name = "USER_GROUP", 
        joinColumns = {@JoinColumn(name = "GROUP_ID", referencedColumnName = "ID")}, 
        inverseJoinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")})
    @OrderBy("userName")
    private Set<User> userList;

    // getter setter ...
}

I created a form to edit users. This form has a lot of inputs and one is for adding and removing groups from the user. It is a simpel primefaces selectCheckboxMenu. This allowes the user to choose new groups and remove old groups. The value(s) is/are directly retrieved from the user entity and the values which can be selected are served by an seperate service.

<p:selectCheckboxMenu id="groupList" multiple="true" filter="true" filterMatchMode="contains"
                      value="#{editUserBean.authUser.groupList}"
                      required="true" requiredMessage="At least one group is needed"
                      converter="cs.argos.gui.jsf.converter.groupConverter" collectionType="java.util.LinkedHashSet">
    <f:selectItems value="#{groupService.groups}" var="group" itemValue="#{group}" itemLabel="#{group.authGroupName}"/>
</p:selectCheckboxMenu>

Converter:

@FacesConverter(value = "groupConverter", managed = true)
    public class GroupConverter implements Converter {

    @Inject
    GroupService groupService;

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {

        if(null == value || value.isEmpty()) {
            return null;
        }

        try {
            return groupService.getGroupWithCollections(Integer.parseInt(value));
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage("Invalid Group ID"), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {

        Group group = (Group) value;

        if (null == group) {
            return "";
        }

        if(null != group.getId()) { 
            return ""+group.getId();
        } else {
            throw new ConverterException(new FacesMessage("Invalid Group ID"));
        }
    }
}

When the used hits a button I'm calling the following service:

@Transactional(Transactional.TxType.REQUIRED)
public User persistWithDeps(User user2Save) {

    User user = user = baseEm.contains(user2Save) ? user2Save : baseEm.merge(user2Save);

    // Check if we have a new object (case when: baseEm.merge(user2Save))
    if (user != user2Save) {
        if (null != user.getGroupList()) {

            // remove groups
            if (!user2Save.getGroupList().containsAll(user.getGroupList())) {

                // remove groups from user
                Iterator<Group> iterator = user.getGroupList().iterator();
                while (iterator.hasNext()) {
                    Group group = iterator.next();

                    if (!user2Save.getGroupList().contains(group)) {
                        group.getuser2SaveList().remove(user);
                        iterator.remove();
                    }
                }
            }

            // Add groups to user
            user.getGroupList().forEach(group -> {
                if (!group.getUserList().contains(user)) {
                    group.getUserList().add(user);
                }
            });
        }
    }

    baseEm.persist(user);
    return user;
}
1

There are 1 answers

0
AudioBubble On

It is JPA related. You put the cascade on the wrong entity:

@ManyToMany(fetch = FetchType.LAZY, targetEntity = Group.class, mappedBy = "userList", , cascade = {CascadeType.MERGE, CascadeType.PERSIST})

Then, you do persist i.s.o. merge:

baseEm.merge(user);

Persist must only be used for creating new objects. Since you are updating an object, use merge.

Also, the method persistWithDeps is written in a very bizar way. When Primefaces correctly updates the groupList in the User object, this is sufficient:

public User persistWithDeps(User user2Save) {
    return baseEm.merge(user2save);
}

JPA will automatically generate the necessary SQL for removing or adding the group entity in the many to many table.

Also be aware that the add and remove operations are based on equals and hashcode behind the scenes. They must be correctly programmed, and if not it can have the effect that nothing happens.

So, try to replace the method with mine. If it still doesn't work, put a breakpoint on return baseEm.merge(user2save); and check user.getGroupList(). It must have the correct number of groups you expect. If it is like this, and the merge still doesn't work, you need to change your equals and hashcode methods. If on the breakpoint you have an incorrect number of groups you didn't expect, it means something went wrong between your JSF linked bean (editUserBean) and the call to persistWithDeps.

Btw, your selectCheckboxMenu tag and converter looks ok.

Good luck!