2

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;
}
Sebastian S.
  • 493
  • 1
  • 5
  • 17
  • 1
    "code does not work" is not that self explaining. As you did not clarify how it does not work (error message in log, console or browser, methods not invoked anyway, so many other possibilities) I can only guess that your code does not compile becaus you use `user2Save` as varialbe and type. – Selaron Nov 20 '18 at 08:38
  • Sorry for my bad explanation. The codes does compile, this was a type caused by stripping down my example for stackoverflow. The problem is, that I cant delete any group from the list. Hibernate does not delete relation – Sebastian S. Nov 20 '18 at 09:42
  • Hence a [mcve] is always required. And did you try to add ajax to your select to keep track of what is and what is not selected? – Kukeltje Nov 20 '18 at 09:42
  • 1
    I can try to create a more complete example, later. Yes, I added some ajax and everything is just fine. I pass the correct list to the merge/persist method but hibernate does not execute any delete statement. – Sebastian S. Nov 20 '18 at 09:46
  • 1
    Then it is not a jsf(2.3) or PrimeFaces issue but a pure hibernate (or even JPA) question. Maybe creating a simple unittest would be better \ – Kukeltje Nov 20 '18 at 09:49

1 Answers1

0

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!