-
-
Notifications
You must be signed in to change notification settings - Fork 611
Description
Describe the bug
According to the docs (https://mikro-orm.io/docs/identity-map#how-does-requestcontext-helper-work) it should be pefectly safe to use a more "global" EM instance from within a transactional or request context (i.e. not having to pass down the EM instance passed to the transaction callback).
However, this expectation breaks in at least one specific scenario that is very easy to replicate.
When the first time a repository is accessed is within a transaction but from the "outside" EM instance (the one on which .transactional was called) then the repository will get "stuck" with the transaction-specific EM instance (can be verified by comapring em.id) which in the best case leads to DriverException: Transaction query already complete and in the worst case can cause data corruption due to improper isolation.
This problem can occur naturally when using MikroORM with NestJS and having services that call one another while each relies primarily on the (global) injected EM instance.
It is worth noting that .getContext() on the "stuck" EM instance is a no-op and returns self, thus the repository is corrupted for the entire lifetime of the application process.
Reproduction
Reproduction:
const orm = await MikroORM.init({});
// EM like the one injected in NestJS
const em = orm.em.fork({ useContext: true });
await em.transactional(async _ => {
// this could happen in a nested service without access to tx-specific EM
await em.getRepository(Post).find({id: 1});
});
// some time later, even in a different request
await em.getRepository(Post).find({id: 1});
// ^ DriverException: Transaction query already complete, run with DEBUG=knex:tx for more info
// the following does not work anymore, the EM of this repo is forever "stuck"
await em.getRepository(Post).getEntityManager().getContext().find(Post, {id: 1});
// em.id !== em.getRepository(Post).getEntityManager().id If the repository happened to be accessed prior to a transaction, the issue does not happen:
const orm = await MikroORM.init({});
const em = orm.em.fork({ useContext: true });
// First repo access is outside a transaction
await em.getRepository(Post).find({id: 1});
await em.transactional(async _ => {
await em.getRepository(Post).find({id: 1});
});
await em.getRepository(Post).find({id: 1}); // works!
// em.id === em.getRepository(Post).getEntityManager().id Using tx-specific instance or explicitly calling .getContext() inside the transaction also resolves the issue:
const orm = await MikroORM.init({});
const em = orm.em.fork({ useContext: true });
await em.transactional(async emtx => {
await emtx.getRepository(Post).find({id: 1});
await em.getContext().getRepository(Post).find({id: 1});
});
await em.getRepository(Post).find({id: 1}); // works!
// em.id === em.getRepository(Post).getEntityManager().id What driver are you using?
@mikro-orm/postgresql
MikroORM version
6.1.12
Node.js version
18.16.0
Operating system
MacOS
Validations
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Slack.
- The provided reproduction is a minimal reproducible example of the bug.