Skip to content

Inintended caching of EntityManager by EntityRepository from within a transactional context #5395

@hesalx

Description

@hesalx

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions