Events

ArcadeDB allows hooking listener to the following events on records (vertices, edges, documents):

  • before is created, by registering the interface BeforeRecordCreateListener

  • after is created, by registering the interface AfterRecordCreateListener

  • before is read, by registering the interface BeforeRecordReadListener

  • after is read, by registering the interface AfterRecordReadListener

  • before is updates, by registering the interface BeforeRecordUpdateListener

  • after is updated, by registering the interface AfterRecordUpdateListener

  • before is deleted, by registering the interface BeforeRecordDeleteListener

  • after is deleted, by registering the interface AfterRecordDeleteListener

The listeners above can be installed and removed at database by using:

database.getEvents().registerListener()

And at specific type level by using:

database.getSchema().getType(<type-name>).registerListener()

All the interface listeners that work before a record is created, read, updated or deleted, require to return a boolean value. If the callback returns true, the listener chain continues and all the following listeners are invoked. If false, the chain of calls is interrupted and the operation is skipped with no errors. In case an error is requested, the callback can throw an exception instead of returning false.

A write a before listener skipped is not counted as applied anywhere a count is replied: the count of a SQL DELETE or UPDATE (and so the Postgres wire and the HTTP API reply), the records an INSERT returns, the n/nModified/upserted of a MongoDB insert, update or delete, the integer of a Redis HSET or HDEL, and the inserted/updated counters of a gRPC insert, where a vetoed row is counted as ignored. A Java caller that counts its own writes learns the answer from MutableDocument.saveReportingVeto(), which returns false for a skipped create or update, where save() returns the document whatever happened. The lower-level DatabaseInternal.createRecordReportingVeto(), updateRecordReportingVeto() and deleteRecordReportingVeto() answer the same for each operation (since v26.10.2).
Over the remote Java drivers (HTTP RemoteDatabase and gRPC RemoteGrpcDatabase) a record cannot be skipped silently, because the caller is not the process that registered the listener: save() of a document, a vertex or an edge a server side listener refused raises RecordVetoedException, for a create and for an update alike, and saveReportingVeto() answers false instead. The driver statistics (createRecord, updateRecord) count only the writes the server applied. Before v26.11.1 a vetoed update was reported over gRPC as a success, over HTTP as a RecordNotFoundException, and a vetoed create over HTTP as a NoSuchElementException; the update of a remote edge that no longer exists, which used to succeed silently, now raises RecordNotFoundException as a document’s does (since v26.11.1). vertex.newEdge() of a remote vertex raises RecordVetoedException too, with the message the embedded engine builds (the edge type and both endpoints), where it used to fail with a bare java.util.NoSuchElementException because the server answers no row for a refused CREATE EDGE (since v26.11.1). vertex.moveToType() and vertex.moveToBucket() of a remote vertex now go to the server (the immutable vertex used to call itself and the mutable one reached for an embedded database it has none of), and raise RecordNotFoundException for a vertex that no longer exists, as countEdges() does.
An edge is the one write that has to return what it created, so Vertex.newEdge() cannot answer "skipped": when a BeforeRecordCreateListener refuses the edge it raises RecordVetoedException, a ValidationException that every protocol reports as the request being refused and not as a server fault, and nothing is stored or linked. The same refusal fails an openCypher CREATE and a Gremlin addEdge(), while SQL CREATE EDGE, like SQL INSERT, skips the refused edge and returns no row for it. A Java caller that counts its own writes uses GraphEngine.newEdgeReportingVeto() (through DatabaseInternal.getGraphEngine()), which returns null for a refused edge (since v26.11.1). A lightweight edge owns no record, so no record listener is ever consulted for it. An asynchronous create (database.async().createRecord(), newEdge()) of a record a listener refused fires neither its success callback nor its error callback, because nothing failed and nothing was created: an application that waits on the callback to learn the outcome must expect that silence for a refused record.
A full scan of a type or bucket (iterateType(), iterateBucket(), or a query that scans a type) fires the read listeners when it reads each record, once per record, even for records your code then never reads. A record a before-read or after-read listener filters out is not returned by the scan (since v26.10.1).
A read listener runs while the database holds its shared lock, and a schema change (creating or altering a type, a property or an index) needs the exclusive one, which a thread holding the shared lock can never be granted. A schema change issued from a before-read or after-read listener therefore fails with a DatabaseOperationException ("a read lock cannot be upgraded"), and the read fails with it. Before v26.10.2 the thread waited for itself forever. Run the schema change outside the listener.

The typical use cases for the listeners are:

  • listen before a create or update to enhance the record with additional properties

  • listen before a create or update to validate properties and in case the record is not valid, returning false or an exception to avoid the operation is executed

  • execute cascade operations. This is the typical use case for AfterRecordDeleteListener where a cascade deletion of multiple connected records can be executed

  • listen to after create, read, update and delete operations to propagate changes to the external or the webapp via web-socket. This allows to have a reactive application that doesn’t poll the database for changes, but rather listens and receives updates as soon as they occur

  • implement custom profiling on changes to the database (by implementing "before" listeners)

Example of before-record-create listener where vertices with "validated" field equal to false cannot be saved (callback returns false):

database.getEvents().registerListener((BeforeRecordCreateListener)
  record -> record instanceof Vertex && record.asVertex().getBoolean("validated"));

The same by only for vertex type "Client":

database.getSchema().getType("Client").getEvents().registerListener((BeforeRecordCreateListener)
  record -> record.asVertex().getBoolean("validated"));