Bulk writes
`updateMany` and `deleteMany` run the write hooks once per row and hand each call the shape of the whole batch, but a `before*` hook and an `after*` hook do not see the same set of rows.
A conditional write picks its rows twice: once when it selects candidates, and again inside the transaction when it locks them and rechecks the predicate. Your hooks run on both sides of that gap, so they see different sets.
Per row, with the batch attached
updateMany({ where, data }) and deleteMany({ where }) run the write stages
once per row, and hand each call four extra keys describing the batch around it.
.hooks({
afterChange: async ({ data, isBatch, count, queue }) => {
if (isBatch && count && count > 100) return; // reindex the whole table instead
await queue.reindexPost.publish(
{ id: data.id },
{ idempotencyKey: `reindex-post:${data.id}` },
);
},
})| Key | What it holds |
|---|---|
isBatch | true when this call is part of a bulk operation. |
recordIds | The ids in the batch. |
records | The rows. As loaded in before*, as written in after*. |
count | How many. |
They ride on beforeChange, afterChange, beforeDelete and afterDelete,
and nowhere else. beforeValidate still runs once per candidate, but with no
view of the batch. Branch on isBatch before reading any of them.
The two calls also skip stages. updateMany fires beforeOperation once for
the whole call, not once per row. deleteMany never fires beforeOperation or
afterRead for the delete, so anything that has to see every removed row
belongs on beforeDelete or afterDelete. Both load their candidates through
an ordinary find, so those two stages do fire for that scan, with
operation: "read".
Intent and fact
beforeValidate, beforeChange and beforeDelete run on the candidates the
where matched. A candidate can still lose the write-time claim to a concurrent
writer and never be touched, so treat these hooks as intent and keep them safe
to run speculatively.
afterChange and afterDelete run only for rows that were actually written.
Their recordIds, records and count describe the winners, not the
candidates, so the same batch can report a smaller number after the write than
before it.
Anything that must reflect a real committed change belongs in an after* hook.
One rejection stops the batch
The chains run one at a time, never in parallel. A throw from a before* hook
cancels the write before it starts. A throw from an after* hook rolls back
every row, not just its own.
updateBatch is not a batch
updateBatch({ updates: [...] }) applies a different patch to each id. It runs
the ordinary single-row update lifecycle once per item inside one shared
transaction, so the batch keys stay undefined throughout. A failure on the third
item still rolls back the first two.
Related
- Hooks for the stages themselves.
- Side effects for the transaction these hooks run in.
- CRUD for the bulk methods.
Side effects
The `after*` write hooks run inside the transaction that made the write, so what you may safely do in one depends on whether the work can join that transaction or has to wait for the commit.
Transition hooks
A workflow stage move is not a write, so the two hooks around it carry their own context, and only the first of them can stop the move.