Persistence tools do not detect dirty entity in property listener - bug?

Hi,

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.

Thanks for the support.
-b

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.

There is some information here:
https://doc.cuba-platform.com/manual-7.1/query.html

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:

em.find(MyEntity.class, entityId);

em.createQuery("...", MyEntity.class).getResult();

But should not do this, it will lead to implicit flush:

em.reload(MyEntity.class, entityId, "myentity-export");

em.createQuery("...", MyEntity.class)
    .setViewName("myentity-with-links")
    .getResult();

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();

1 Like

Hi Alex,

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?

Thanks for bearing with me, much appreciated!
-b

Seems the FlushMode doesn’t help.

That is, I coded a ‘safe’ reload query:

 @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.

Any help appreciated.
-b

Is it possible to have the listeners executed in a defined order so it assures some of my preconditions?

I did read this info in the manual:

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:

  1. For each ancestor, starting from the most distant one, dynamically added listeners are invoked first, followed by statically assigned listeners.
  2. 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’m afraid I’m stuck here.

-b

So, let me share an update here.

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.

Better solutions are highly appreciated !
-b

Hi Berend,

I’m really wondering if the issue you are running into is not the same that I described here:

To me it looks the same problem.

Samy

Hi Samy,

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.

Thanks for sharing your thought thou.
-b

Consider using EntityChangedEvent which encourages you to work with detached entities through DataManager or TransactionalDataManager.

Thanks @knstvk. As this is not included in the 6.x manual I guess it is to be used in release 7? Will see to adapt to this approach in the future.

It’s available since CUBA 6.10, though documented in 7.0+.