Triggers
Triggers are hooks that Ent Framework executes right before or after a mutation (insert, update or delete). They can modify the table row before it's got saved, and also load or update other Ents.
The word "hook" also draws the analogy with React Hooks (from frontend world), since update-triggers in Ent Framework have several traits in common with React's useEffect()
hook.
Triggers are defined in the Ent Class configuration, near Privacy Rules.
Before-Triggers
In before-triggers, you can:
Make changes in the fields right before they are saved to the database.
Load or even mutate other Ents.
beforeInsert Triggers
Let's start with an example:
Here, we automatically assign the value to slug
field of the inserted row based on the topic's subject.
Notice that it requires a little quirk though: slug
field in the schema needs to be defined with autoInsert
attribute, otherwise Ent Framework TypeScript typing will make slug
as a required property in `insertReturning()
call.
As everything in Ent Framework, all arguments of the trigger functions are strongly typed.
It will respect the field types defined in the schema exactly.
Nullability is respected (fields defined with
allowNull: true
will be nullable in theinput
argument).It will pay attention to required and optional fields (the optional fields are the ones defined with
autoInsert
orautoUpdate
).
Accessing Ent ID in beforeInsert Trigger
Despite the insert operation has not yet been applied to the database, in all beforeInsert
triggers, you can already read the ID of the Ent to be inserted.
This is very convenient to organize eventually consist logic in your code: in Ent Framework, there are no transactions exposed (and there can be no transactions even in theory when working across microshards or across different storage services), so you must pay attention to the order of the writes, to make sure your don't lose eventual consistency behavior:
Here we assume that you have a addToKafka()
function which accepts the Ent class name and the Ent ID. After the write to Kafka succeeds, you proceed with saving the Ent to the database. Using this apprpach, you can e.g. implement eventually-consistent pipelining of the Ent data to some other storage using an external bus (like Kafka or Redis Streams), despite this system "bus+PostgreSQL" being not transactionally safe as a whole.
beforeUpdate Triggers
Update is a more complicated operation, since you have the old row and the new row versions at the same time.
Notice that the code here is very similar to the beforeInsert
trigger we discussed above. To avoid boilerplate in such cases, you can use beforeMutation
instead; we'll describe it a little later.
In the trigger functions, Ent Framework gives you the following arguments:
oldRow
: the row with Ent fields right before the update. This object is immutable.input
: properties passed toupdate*()
method as they are. Notice that it includes not all Ent fields, but only the fields you are mutating (in other words, all properties ofinput
object are optional in their TypeScript typing). You need to modify this object if you want the trigger to make changes in the Ent before the update happens.newRow
: the result of applyinginput
overoldRow
. This is an immutable object.
Immutable Fields
Using beforeUpdated
, you can force some Ent field to be immutable, so any update*()
call will not change it:
beforeDelete Triggers
This kind of triggers is the simplest:
In this example, we do two things:
We call
addToKafka()
function to e.g. publish the deletion event to our event bus, so we can replay that deletion to some other data store in an eventually consistent manner. If publishing to Kafka fails, them the trigger will throw an error, and no deletion will happen in the database. If deletion succeeds, then we can be sure that it also got replayed to Kafka (since it's done prior to the deletion). And if deletion fails... then the user will see it and retry later.We delete all children comments when the topic is deleted. This is a kind of
ON DELETE CASCADE
clause in relational database's foreign keys, but with an important difference: it calls Ent Framework triggers on the comments as well.
beforeMutation Triggers
Notice that we have some boilerplate in our triggers:
We call
addToKafka()
in 3 places:beforeInsert/Update/Delete
triggers.We have the exact same logic to calculate
slug
field in 2 places:beforeInsert/Update
.
To eliminate that, there is a special feature: beforeMutation
triggers, which are called before any mutation (be it insert, update or delete), in a TypeScript-safe way for the arguments.
There are 2 gotchas here:
We split one big trigger into two independent ones. The triggers are run sequentially, and the next trigger in the list is not called if the previous one throws an error.
TypeScript is smart enough to understand that, when you check
op
against"INSERT"
or"UPDATE"
strings, the typing ofnewOrOldRow
andinput
arguments will be according to the operation types (i.e. it will respect optional properties for instance).
Changed Fields Tracking and React's useEffect() Analogy
But you are probably still not satisfied with that long if
clause in the example above. We can improve the code:
Here we pass a tuple with 2 lambdas:
The 1st lambda,
(vc, row) => [row.subject]
, is called "deps builder". It extracts some part of the row, and Ent Framework will call the trigger code only if that part has actually changed on an update (and also on insert/delete, since those are also considered as "changes")The 2nd lambda is your trigger code. Ent Framework will run it only if the 1st callback returned a value different between the old and the new rows (or it's an insert or delete operation). If you are familiar with React, you can notice that this mechanism is similar to how its
useEffect()
hook works.
In the trigger code, you still need to check that the operation is not DELETE
, but it is way better still than having a boilerplate in the previous example.
The "deps builder" lambda can be async, so you can run other database queries in it and make decisions based on their results.
Notice that "deps builder" tuple also works for beforeUpdate
, as well as for afterUpdate
and afterMutation
triggers we'll discuss below.
After-Triggers
After-triggers are called seqentially, as soon as an insert/update/delete mutation succeeds in the database.
afterInsert Triggers
Triggers of this kind act exactly as beforeInsert
, but they are called after a successful database operation, not before. There, you can do some auxiliary work, but keep in mind that, if this work fails, the Ent will remain created in the database still. There are no (and cannot be) built-in transactions across multiple independent IO services and multiple different microshards.
afterUpdate Triggers
The only difference with beforeUpdate
triggers here is that there is no input
argument passed: the only things you have are oldRow
and newRow
:
You can also use "deps builder" syntax in afterUpdate
, to run the trigger code only if some particular fields change:
afterDelete Triggers
In afterDelete
, you can run some optional cleanup of other resources associated to the just-deleted Ent. Keep in mind though that it's all non-transactional: if your cleanup fails, it won't be retried, and the row will already be deleted in the database.
afterMutation Triggers
Similarly to beforeMutation
triggers, afterMutation
triggers allow you to react on any of insert/update/delete operations. but only after this operation succeeds in the database.
There is also no input
argument available in this kind of triggers, only newOrOldRow
.
You can use "deps builder" syntax too if you want to react only when some particular fields change on an update (or on insert and delete unconditionally):
Last updated
Was this helpful?