How to store DB transaction object that is available only within a callback?
sam-artuso opened this issue · comments
Some DB libraries manage a DB transaction within a callback.
Here's an example of what Sequelize calls a "managed transaction":
await sequelize.transaction(async (t) => {
// queries in here can use `t`
});
Heres' another (very similar) example with pg-promise
:
await db.tx(t => { /* ... */ })
The advantage is that the commit or rollback are automatic based on whether an error is thrown within the callback.
I'm having a hard time figuring out how to store t
in AsyncLocalStorage globally. My sense is that I should use an interceptor, but I'm not sure how?
In short, basically the same way you did here. But you also need to make sure to remove the transaction object from the context once the transaction finishes.
This can be done in two ways:
- Re-use the same CLS-context
// if we're in a nested transaction, remember the previous one
const prevTx = cls.get('transaction')
// wrap the call in managed transaction
await db.tx(async t => {
try {
cls.set('transaction', t)
return await someService.doStuff()
} finally {
// make sure to replace the transaction with the original value
// otherwise it will be accessible even after the transaction is complete
// which will cause errors in case you attempt to re-use it
cls.set('transaction', prevTx)
}
})
- Create a nested CLS-context around the call
// make sure to inherit the existing context, otherwise it will be empty
cls.run({ ifNested: 'inherit' }, async () => {
return db.tx(async t => {
// this sets the transaction on the nested context only
cls.set('transaction', t)
return await someService.doStuff()
})
})
The latter one has the nice property that CLS-enabled transactions will work even if no context was initialized prior.
Note: Sequelize supports CLS-enabled transactions out of the box
I'm not sure I made myself clear. My question is about how to link an HTTP request to a fresh db transaction:
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
setup: (cls, req) => ???,
},
}),
P.S.: this is for an HTTP context, not a "CLI" context like in this other question of mine.
I see, well, if you want to wrap the entire request with a transaction, which I would not recommend for the reasons listed below (*), you cannot do it via the ClsMiddleware
setup. That function is there only to populate the context, but you can't further wrap the execution of the flow. For that, you'd need to create a custom middleware or interceptor that wraps the next()
(or handle.next()
) call with what I shared earlier (replacing someService.doStuff()
with the next
call).
(*) Since wrapping code with transaction effectively executes the transaction at the database level, it is advised to only start it when you absolutely need it as close to the actual database calls as possible. If you implicitly wrap each request, then you'll end up with an empty transaction a lot of times, in case the request flow never makes it to the database call, or you'll be unnecessarily locking the database for simple SELECT
queries.
Yes, it's true, my approach would create a lot of empty transactions and has overhead. Still, some people do it because it's simple.
Do you have any suggestions on how to create a decorator that I can just drop at the point closest to where I need the transaction to start?
As a matter of fact, I do. And I am currently in process of releasing a first draft of the @nestjs-cls/transactional
plugin (#96). There is a PR ready (#102) so you can look through the code (although it's a bit complicated, given it should be generic).
There are two important parts there: The TransactionalHost
provider, with the universal withTransaction
method used to wrap pieces of code in a transaction, together with the TransactionalAdapter
which defines how to wrap the piece of code with a transaction based on a specific database connection object, and the @Transactional
decorator which makes Nest inject TransactionHost
into the class instance and wraps the actual method with the TransactionHost#withTransaction
call. The transactional client itself is available on TransactionHost#client
.
You can also look at the unit test suite about how it is used.
In the following days, I plan on releasing version 4
of nestjs-cls
together with the transactional plugin and a Prisma adapter for starters and then continue implementing other adapters as well.
Please see the @nestjs-cls/transactional
plugin.