enhancement
major
minor
defect
critical
major
minor
major
#30225
Model: handlers declared on an attribute (change) or a type (creation, deletion), run within the commit
Related: #30218.
Problem
An application that must keep data consistent whenever a certain attribute changes - whoever changes it (a view form, a table edit, an import, a script) - needs a ModificationListener. Such a listener can only be registered in Java (KnowledgeBase.addModificationListener, typically from a service of the application), is called for every commit of every session and has to filter the created / updated / removed maps by type and attribute itself. The updated map does not even tell which columns of an item changed, and a reference stored in a link table changes rows of another table than the one of the owning object.
The model already offers a declarative, per-type hook for creation: TLObjectInitializer`s declared on a type are applied to new objects by the `ModificationListener of ModelService. There is no counterpart for changes or the deletion of an existing object.
Example: E-Home (haumacher/ehome #111) rewrites every expression naming a rule parameter when the id of the parameter is renamed.
Lösung
Change handlers on an attribute
The attribute annotation <change-handlers> (TLChangeHandlers) lists handlers (TLChangeHandler) that are called when a transaction changing the value of the annotated attribute of an existing object is committed - independent of the source of the change. changed(object, attribute, oldValue, newValue, CommitActions) runs within the commit before the commit checks (model constraints); its changes become part of the same commit.
- The old value is the committed value; a value set and set back within the transaction is no change. Objects created or deleted in the same transaction are not reported.
- Supported are all attributes whose values are stored in the database: columns (also flexible attributes), link tables, references stored in the rows of the referenced objects, formatted text, and the reverse ends of references (a handler on a reverse reference is called for the old and the new referenced object).
- Loading the model rejects <change-handlers> on an attribute whose changes cannot be detected (derived, transient, computed values). For this, the model loader checks all annotations implementing TLAnnotationTargetCheck in a late phase, when all parts are complete.
- Handlers of overridden attributes are inherited unless ignore-inherited is set.
- Only attributes that carry handlers are inspected: the attribute's storage determines from the raw changes of the commit which objects may have changed (StorageImplementation.addChangeLocators), indexed by table in the model cache. A model without handlers pays nothing.
Lifecycle handlers on a type
The type annotation <lifecycle-handlers> (TLLifecycleHandlers) lists handlers (TLLifecycleHandler) informed about objects of the type and its specializations (handlers of generalizations first):
- created(object, CommitActions): in the commit of the creating transaction, after the `TLObjectInitializer`s of all created objects, before the change handlers and the commit checks.
- Modification deleting(object, CommitActions): two-phase. It is called when the object is deleted, while the transaction is locked (the object can be read, nothing may be changed), and returns the changes that run after the object and its cascade have been removed, within the deleting transaction.
- Objects created and deleted in the same transaction are reported neither as created nor as deleted.
TL-Script
- <change-handlers><script operation="..."/></change-handlers> (ChangeHandlerByExpression): the script receives the object, the new value, the old value and the attribute.
- <lifecycle-handlers><script created="..." deleting="..."/></lifecycle-handlers> (LifecycleHandlerByExpression): the deleting script reads the object and returns a function (or nothing) that is executed after the deletion.
- The scripts run without security checks, like the model constraints: they are consistency logic declared in the model.
Common rules
- Changes made by handlers do not trigger change or creation handlers again. Deletions are reported with each delete, so an object deleted by a handler is reported to the deletion handlers of its type.
- A Java handler reports to the user by registering an AfterCommitAction on the CommitActions it receives (#30218). There is no dedicated message support for TL-Script handlers.
- The initializers of created objects are applied in a flat loop instead of a chain of Modification.andThen() whose nesting grew with the number of created objects.
- Developer article model/change-handlers (when to use which mechanism, timing, examples).
Prepared raw changes for ModificationListener
ModificationListener.createModification receives one PendingChanges object instead of the three maps of created, updated and removed items. Besides these maps it groups the items by table and tells for an updated item which attributes (columns and flexible attributes) changed and what their committed value was, computed from the committed and the transaction-local values of the item without building the ItemChange`s of the later `UpdateEvent.
Migration
- ModificationListener.createModification(KnowledgeBase, CommitActions, Map created, Map updated, Map removed) becomes createModification(KnowledgeBase, CommitActions, PendingChanges changes). Implementations replace the three map parameters by PendingChanges and read the maps through changes.getCreated(), changes.getUpdated() and changes.getRemoved().
- An implementation of StorageImplementation that implements both ColumnStorage and SeparateTableStorage must override addChangeLocators(...) and canDetectValueChanges() to combine both behaviours (the compiler reports the conflicting default methods).