I have a property listener that catches changes in entities with regard to the owner (= a user) of that specific object. When it changes and it does not change to the current user, an email is sent to this new owner.
This works fine for newly created entities and for changes to entities directly. However, when the entities are in association of another entity and the owner is changed while editing the main entity.
Although the listener is triggered and the associated entity is included in the set of managed entities, this specific entity is not marked dirty when using the persistence tools.
Listener
@Override
public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) {
// Loop all managed entities
for (Entity e : managedEntities) {
// Don't do this for existing non-dirty entities
if (!persistenceTools.isDirty(e) && !PersistenceHelper.isNew(e))
continue;
// Check if the owner changed
User newOwner = null;
if (persistenceTools.isDirty(e, "owner")) {
newOwner = e.getValue("owner");
}
// Check if someone needs to be informed (yes, except when assigned to current staff)
User curUser = userSession.getUserSession().getUser();
if ((newOwner != null) && !curUser.equals(newOwner)) {
// Send email ...
}
}
}
The check persistenceTools.isDirty(e) gives a false while the owner actually changed. Same is true for persistenceTools.isDirty(e, "owner").
It seems a bug or am I missing something? I’m using release 6.10.13.
Ok, found out that an entity listener was messing up the dirty flag. Within the entity listener there is a reload of the main entity which causes the original entity to be somehow cleared of its dirty flag. Although it has not been saved according to the database records.
It seems a bug to me. I will try to get a test project assembled that indicates the problem (or so I hope).
-b
Hi,
Because of some EclipseLink pecularities, in CUBA there is a thing called “implicit flush” which can trigger in a long transaction every time you do a entity reload with view or executing a JPQL query with view. Implicit flush sends all non-committed changes to the database and of course clears dirty state.
In the entity listeners, if you execute queries to load entities, you shouldn’t specify view name or view. It leads to lazy-loading of unloaded associations but prevents implicit flush of unsaved changes.
So you have to be very careful in the entity listeners code.
Basically, you can safely do this in the BeforeXXXListener code:
You can debug implicit flushes in your code by putting a breakpoint to the
com.haulmont.cuba.core.sys.persistence.PersistenceImplSupport#beforeStore
method in this place:
if (warnAboutImplicitFlush) {
statisticsAccumulator.incImplicitFlushCount();
Great - this explains exactly what was going on. Thank you for diving into this given my cryptic description.
I already was able to fix some scenario’s but having difficulty when a property is to be tested that is not within the local view (as used by default on em.find()), an unloaded association. You mention:
It leads to lazy-loading of unloaded associations but prevents implicit flush of unsaved changes.
However, there is no actual (lazy) loading of these properties when requested. How to proceed in such a case? Should there be a query with the FlushModeType.COMMIT set? Or is there another approach possible?
@SuppressWarnings("unchecked")
private <T extends Entity> T safeReload(EntityManager em, T o, String view) {
return (T) em.createQuery("select o from " + o.getMetaClass().getName() + " o where o.id = ?1")
.addView(o.getMetaClass().getJavaClass(), view)
.setParameter(1, o.getId())
.setFlushMode(FlushModeType.COMMIT)
.getSingleResult();
}
But after running the query, it still marks the entity as not dirty anymore.
If several listeners of the same type (for example from annotations of entity class and its parents and also added dynamically) were declared for an entity, they will be invoked in the following order:
For each ancestor, starting from the most distant one, dynamically added listeners are invoked first, followed by statically assigned listeners.
Once parent classes are processed, dynamically added listeners for the given class are invoked first, followed by statically assigned.
So, if I would add the owner notification listener dynamically it would go first?
Update: this is not possible as the owner notification listener is a transaction listener which is executed after the entity listeners:
BeforeCommitTransactionListener beforeCommit() method is called before transaction commit after all entity listeners if the transaction is not read-only.
I could not get a reload without destroying the dirty flags on related entities. But, I managed to work around the limitation by:
Wherever possible, stick to em.find() (as suggested by @AlexBudarov)
Carefully code all actions in that they do what needs to be done in the right order - this sometimes is hard but avoids some problems
Do more general queries with the setFlushMode(FlushModeType.COMMIT), this seems to work ok when loading a general list of entities (rather than a reload)
If everything else falls short, use DataManager instead of EntityManager. Although it seems a bad practice to do so in listeners, I found this to work ok. Why? When doing a dataManager.reload() the entities get committed in a way that also triggers the transaction listener whereas entityManager.reload() does not.
I doubt it is the same as the issues I’m describing are on the core tier using entity listeners and transaction listeners. This has very little to do with listeners on the client/web tier.