This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Entity Listener

Entity listener are used to make changes to entities or run logic when they get created, updated or deleted before the database transaction is committed (except CollectingAfterCommitListener). Listeners listen to events (create, update, delete) of defined entities. A listener is registered on at least one entity model. It is possible to register the same listener on multiple entity models.

Listeners are spring beans which should be annotated with ch.tocco.nice2.persist.core.api.entity.events.Listener. The filter property of the annotation can be used to restrict to which entities the listener should be applied.

Examples:

Note

  • Restrict to a single entity: @Listener(filter = "User")

  • To apply the listener to multiple entities an array should be used: @Listener(filter = {"User", "Address"})

See Listeners for a more technical documentation of the listener.

Warning

Do not use Entity Listeners if the goal also can be reached by Default-Values

Dependencies

Support for entity listeners is added by the core:persist:core module:

dependencies {
     implementation project(":core:persist:core")
}

It is however very likely that this module is already transitively included by another module.

CollectingEntityListener

Collecting entity listeners fetch all EntityFacadeEvent until the current transaction is about to commit. This means a collecting entity listener is executed only once right before the commit. All EntityFacadeEvents which are produced later, for example by an other collecting entity listener which runs after the current, are discarded.

To add a CollectingEntityListener add a Java class which extends the class CollectingEntityListener. The method onBeforeCommit must be overwritten. This is the method which gets called by the nice2 framework just before the commit takes place.

@Listener(filter = "...")
@ThreadScope
public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        // your code goes here
    }
}

Important

The thread scope must be used for collecting entity listeners (@ThreadScope annotation). This means that the service is instantiated once per Thread and bound to this Thread only. This is needed because the collecting entity listener fetches every EntityFacadeEvent of one transaction only.

As long as the @Listener annotation is used, the listener will be registered automatically.

Handle Events

The class CollectingEntityListener provides some useful methods to handle the EntityFacadeEvents correctly. The most important methods are described here. Open the class ch.tocco.nice2.persist.util.CollectingEntityListener to see all methods.

Do something with all created entities

The method withCreatedEntities calls the given Consumer<Entity> with all created entities with the entity model which the listener was contributed to listen to.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        withCreatedEntities(entity -> {
            // Your code goes here
        });
    }
}

Hint

There is also the older method getCreatedEntities which returns a list of the entities. However, this method should only be used if you can’t work with withCreatedEntities for some reason. getCreatedEntities is the less preferred method usually as it’s slower and needs more memory, because it has to create the list to return.

Do something with all updated entities

The method withUpdatedEntities calls the given Consumer<Entity> with all updated entities with the entity model which the listener was contributed to listen to.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        withUpdatedEntities(entity -> {
            // Your code goes here
        });
    }
}

Hint

There is also the older method getUpdatedEntities which returns a list of the entities. However, this method should only be used if you can’t work with withUpdatedEntities for some reason. getUpdatedEntities is the less preferred method usually as it’s slower and needs more memory, because it has to create the list to return.

Do something with all deleted entities

The method withDeletedEntities calls the given Consumer<Entity> with all deleted entities with the entity model which the listener was contributed to listen to.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        withDeletedEntities(entity -> {
            // Your code goes here
        });
    }
}

Hint

There is also the older method getDeletedEntities which returns a list of the entities. However, this method should only be used if you can’t work with withDeletedEntities for some reason. getDeletedEntities is the less preferred method usually as it’s slower and needs more memory, because it has to create the list to return.

Do something with affected entities

The method withAffectedEntities calls the given Consumer<Entity> with all created and updated entities with the entity model which the listener was contributed to listen to.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        withAffectedEntities(entity -> {
            // Your code goes here
        });
    }
}

Hint

There is also the older method getAffectedEntities which returns a list of the entities. However, this method should only be used if you can’t work with withAffectedEntities for some reason. getAffectedEntities is the less preferred method usually as it’s slower and needs more memory, because it has to create the list to return.

Do something with all affected entities

The method withAllAffectedEntities calls the given Consumer<Entity> with all created, updated and deleted entities with the entity model which the listener was contributed to listen to.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        withAllAffectedEntities(entity -> {
            // Your code goes here
        });
    }
}

Hint

There is also the older method getAllAffectedEntities which returns a list of the entities. However, this method should only be used if you can’t work with withAllAffectedEntities for some reason. getAllAffectedEntities is the less preferred method usually as it’s slower and needs more memory, because it has to create the list to return.

Using the Context in Collecting Entity Listeners

The context can be received by the Transaction passed to the onBeforeCommit method.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        Context context = tx.getContext();
        // Your code goes here
    }
}

Important

Do not inject the Context in a CollectingEntityListener but get it from the passed Transaction

The Order of Collecting Entity Listeners

Collecting entity listeners are not ordered by default. However if ordering is necessary for a number of listeners the priority() method can be overridden.

EntityFacadeListener

Note

The InterruptibleEntityFacadeAdapter should not be used any longer as the persistence framework does not throw any InterruptedExceptions any more.

An EntityFacadeListener is executed for every EntityFacadeEvent which belongs to the entity on which the listener is contributed to listen to. This means every time setValue or a similar method is called on the entity the EntityFacadeListener is executed.

To add a EntityFacadeListener add a Java class which implements the class and annotate it with @Listener.

@Listener(filter = "...")
public class MyEntityFacadeAdapter implements EntityFacadeListener { ... }

Now depending on what the listener needs to do there are several methods which can be overridden.

entityCreating

This method gets called if a new entity was created.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityCreating(EntityFacadeEvent event) {
        Entity user = event.getSource();
        // do something with `user`
    }
}

entityDeleting

This method gets called if an entity was deleted.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityDeleting(EntityFacadeEvent event) {
        Entity user = event.getSource();
        // do something with `user`
    }
}

entityChanging

This method gets called if any changes are made to an entity.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityChanging(EntityChangedEvent event) {
        if ("field_name".equals(event.getField().getName())) {
            // do something with `user`
        }
   }
}

EntityChangedEvent

It is important to only process the listener if it is really necessary. Lets say a listener must set a flag isAdult on Users if they’re older than 18 years. This could be done like this:

// Bad example
public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityChanging(EntityChangedEvent event) {
        Entity user = event.getSource();
        if (isAdult(user) {
            user.setValue("is_adult", true);
        }
    }
}

This would work without any problems. But most probably this listener would be executed a lot of times even it would not be necessary. Because EntityFacadeListener are executed every time setValue is called on the entity, this listener is also executed if for example only the name of the user was changed. The name has nothing to do with the age of user.

A EntityChangedEvent is passed to the method entityChanging which has some additional methods to work with. The above example can be rewritten to the following:

// Good example
public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityChanging(EntityChangedEvent event) {
        if ("birthdate".equals(event.getField().getName())) {
           LocaleDate birthdate = (LocalDate) event.getNewValue();
           if (isAdult(birthdate) {
               user.setValue("is_adult", true);
           }
        }
    }
}

Before the whole logic (setting the adult flag) is processed, we check if the change which is done to the entity belongs to the field birthdate because this is the only field which is relevant for this listener. Then instead of reading the field birthdate from the entity we just call the method getNewValue on the A EntityChangedEvent. Because getNewValue returns an Object it needs to be cast first.

entityRelationChanging

This method gets called if a relation on the entity was changed.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityRelationChanging(EntityRelationChangedEvent event) {
        if("relRelation_name".equals(event.getRelation().getName())) {
            // Your code goes here
        }
    }
}

EntityRelationChangedEvent

A EntityRelationChangedEvent is passed to the method entityRelationChanging which has some additional methods to work with.

To check what relation was changed the method getRelation can get called on the EntityRelationChangedEvent.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityRelationChanging(EntityRelationChangedEvent event) {
        Relation relation = event.getRelation();
        String relationName = relation.getName(); // e.g. `relUser`
    }
}

There also methods to check how the relation got changed.

public class MyEntityFacadeAdapter implements EntityFacadeListener {
    @Override
    public void entityRelationChanging(EntityRelationChangedEvent event) {
        if (event.isAdded()) {
            // code is executed when the relation was added to the entity.
            // E.g. User.relUser_status was set to `active`
        }

        if (event.isRemoved()) {
           // code is executed when the relation was removed.
           // E.g. User.relUser_status was cleared
        }

        if (event.isAdjusting()) {
           // code is executed if the relation was changed.
           // E.g. User.relUser_status was changed from `active` to `archived`
        }
    }
}

Avoid Infinite Loops

With EntityFacadeListeners it is possible to create infinite loops. Because these listeners are executed every time a change has made to the entity which the listener listens to. In the picture below an example of an infinite loop is shown (example does not make any sense).

../../_images/listener-entity-facade-listener-infinite-loop1.png

Both listeners listen to changes on the entity User. Listener A listens on changes on the field firstname and sets a value on the field lastname. Listener B listens to the field lastname which was set from the listener A and sets a value on the field firstname. Now listener A again is executed and so on.

CollectingAfterCommitListener

CollectingAfterCommitListeners are fired after the transaction was committed. This can be useful if something only must be done if something else was persisted before. For example mails are sent often with CollectingAfterCommitListeners. Lets say a user should receive an e-mail if he was registered to an event. This could be done within a EntityFacadeListener or CollectingEntityListener.

public class MyCollectingEntityListener extends CollectingEntityListener {
    @Override
    public void onBeforeCommit(Transaction tx) {
        getCreatedEntities().forEach(registration -> {
            sendMailTo(registration); // what if later in an other entity listener something goes wrong?
        });
    }
}

But if the current transaction for some reason fails, it will be rolled back and the registration entity is not persisted. In this case the user would have received an e-mail but was not actually registered to the event.

That is when CollectingAfterCommitListeners are useful.

CollectingAfterCommitListeners need to be annotated with the @Listener annotation the same way as the other listeners.

The listener must extend the class CollectingAfterCommitListener and overwrite the method getAfterCommitTask which returns an AfterCommitTask.

@Listener(filter = "...")
@ThreadScope
public class MyCollectingAfterCommitListener extends CollectingAfterCommitListener {
    public MyEntityFacadeAdapter(CommandExecutor commandExecutor) {
        super(commandExecutor);
    }

    @Override
    protected AfterCommitTask getAfterCommitTask() {
        return new AfterCommitTask() {
            @Override
            public void onAfterCommit(CommandContext commandContext) throws Exception {
                // Your code goes here
            }
        };
    }
}

Note

A CollectingAfterCommitListener does not know what has changed on the entities itself. But if the entity got created, updated or deleted is known. Like the normal collecting entity listener it is necessary to use the @ThreadScope annotation.