0

I have Principal and PrincipalRoles entity classes. They have a bi-directional “parent-child” relationship defined like this:

Principal has:

@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, orphanRemoval = true, mappedBy = "principal")
Collection<PrincipalRoles> rolesByPrincipalCollection;

PrincipalRoles has:

@JoinColumn(name = "principal", referencedColumnName = "id", nullable = false, insertable = true, updatable = true, unique = false)
@ManyToOne(fetch = FetchType.LAZY)
Principal principal;

I also have a master-detail web page in the Web module of a Java EE application. The page uses the entity classes and their corresponding facades contained in the EJB module of the application.

The page obtains the Principal object and later updates it using methods of a Stateless EJB, PrincipalFacadeBean, which basically execute the find and merge methods of the Entity Manager. The Persistence Context is Container-Managed.

The page can add and remove several PrincipalRoles and then merge the Principal successfully. It can also add and remove several PrincipalRoles, update JUST ONE of the previously added and then merge the Principal; but it always fails when attempting to update more than one in a single merge operation. In those cases, no exception is raised but nothing is updated in the database. Not even other fields of Principal, like name, e-mail, etc.

The application runs on GlassFish 5.1.0, Java 1.8.0_281, EclipseLink 2.7.4, PostgreSQL 13.

I’ve been dealing with this problem for a few days now and I'm running out of ideas. Any assistance you can provide would be greatly appreciated.

Addendum to answer @crizzis questions:

I’m not sure what the service method would be. The page bean calls the façade directly using this method:

@Override
public List<Principal> merge(List<Principal> list) {
    return principalFacade.merge(list);
}

In master-detail pages the list contains only the master. This is the façade’s merge method:

@Override
public List<Principal> merge(List<Principal> list) {
    List<Principal> m = new ArrayList<>();
    for (Principal o : list) {
        m.add(_manager.merge(o));
    }
    return m;
}

These are the equals and hashCode methods of PrincipalRoles. Actually, these methods have the same template in all entity classes.

@Override
public boolean equals(Object obj) {
    if (obj instanceof PrincipalRoles) {
        PrincipalRoles that = (PrincipalRoles) obj;
        return that == this || ObjUtils.equals(id, that.id);
    }
    return false;
}

@Override
public int hashCode() {
    return Objects.hashCode(id);
}

This is ObjUtils.equals:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a == null && b == null) || (a != null && b != null && a.equals(b));
}

The principal field of the updated PrincipalRoles is the Principal object being merged (at least their System.identityHashCode matches). I also checked the value of an unmodified PrincipalRoles. That was null.

Jorge Campins
  • 413
  • 1
  • 4
  • 11
  • Please post the relevant service method. How is `equals/hashCode` implemented for `PrincipalRoles`? What is the value of the updated `PrincipalRoles`'es `principal` field when you save them? – crizzis Apr 04 '21 at 08:50
  • To @crizzis: first of all, thanks for answering. I had a lot of touble formatting these comments so I added my answers to the original post. – Jorge Campins Apr 04 '21 at 14:15
  • Where are these PrincipalRoles coming from when being associated to Principal? Have you updated both sides of this Principal-PrincipalRoles relationship, and if these PrincipalRoles are pre-existing, what ever Principal entity they might have been referencing before the change? – Chris Apr 05 '21 at 14:15
  • You might try turning on SQL logging to see queries that are generated and when to verify they match what you expect ( https://wiki.eclipse.org/EclipseLink/Examples/JPA/Logging ). Something like the cascade.refresh could be being triggered depending on your operations and when lazy relationships are accessed. – Chris Apr 05 '21 at 14:18
  • @Chris, thanks for answering. The pre-existing PrincipalRoles come along with the Principal due to the EAGER fetch. The page updates that collection and then calls the Principal facade to merge it. When I activate EclipseLink logging, there is nothing but SELECTs when it fails. But in every scenario that it works (any combination of adding and removing) the log shows the expected SQL statements. – Jorge Campins Apr 05 '21 at 18:08
  • 'updates that collection' - please specify what you mean; are you modifying this existing collection, adding/removing entries, or even moving PrincipalRoles from one Principal to another. The later should be avoided with orphan removal enabled, and could be involved in this issue. Also show the SQLs from the working and non-working cases - they will have differences both in the SQL itself and the when in relation to what you are doing - and If/when you are performing refreshes as I still suspect cascade refresh could cause this, especially if other entities reference these ones. – Chris Apr 05 '21 at 21:14
  • @Chris, I didn't see your last comment until this morning. As you can realize, I found a solution a few hours before you posted it, so I forgot about the problem for the rest of the day... – Jorge Campins Apr 06 '21 at 14:08
  • ...Answering your question, by "update that collection" I meant adding and removing any number of elements and also updating some. The problem only occurred when updating more than one. I guess EAGER fetch for principal in PrincipalRoles avoids the "cascade refresh" you mention. And I wonder, why could it update one? Why that "cascading refresh" only happened when updating more than one (regardless of the number of elements added or removed). Thanks again. – Jorge Campins Apr 06 '21 at 14:09
  • I can only suspect, but you can verify if you turned SQL logging on and check if/when you are using refresh options, but that by having the relationship lazy, the query to fetch a parent is refreshing that parent object, wiping out any previous changes to it. I can't say why that would occur on the model snipped you've shown, only that it seems likely the case based on your solution. The last one to trigger the fetch is likely the one who's changes remain and get picked up/merged in the database. Ie Checkout the state of the object you are passing in to the merge method. – Chris Apr 07 '21 at 16:00

1 Answers1

0

All I had to do was change the @ManyToOne of the field principal in PrincipalRoles from @ManyToOne(fetch = FetchType.LAZY) to @ManyToOne(fetch = FetchType.EAGER). I still don't understand why, but it works! Thanks again to @crizzis and @Chris. Their questions led me to the answer.

Jorge Campins
  • 413
  • 1
  • 4
  • 11
  • The puzzling part of this matter is that the page could add and remove any number of roles and update **one** (and only one). Now I'm wondering, why could it update one? – Jorge Campins Apr 05 '21 at 19:15