Search code examples
mikro-orm

How to use properly EntityRepository instances?


The documentation stresses that I should use a new EntityManager for each request and there's even a middleware for automatically generating it or alternatively I can use em.fork(). So far so good.

The EntityRepository is a great way to make the code readable. I could not find anything in the documentation about how they relate to EntityManager instances. The express-ts-example-app example uses single instances of repositories and the RequestContext middleware. This suggests that there is some under-the-hood magic that finds the correct EntityManager instances at least with the RequestContext. Is it really so?

Also, if I fork the EM manually can it still find the right one? Consider the following example:

(async () => {
  DI.orm = await MikroORM.init();
  DI.em = DI.orm.em;
  DI.companyRepository = DI.orm.em.getRepository(Company);
  DI.invoiceRepository = DI.orm.em.getRepository(Invoice);
  ...
  fetchInvoices(em.fork());
}

async function fetchInvoices(em) {
  for (const company of await DI.companyRepository.findAll()) {
    fetchInvoicesOfACompany(company, em.fork())
  }
}

async function fetchInvoicesOfACompany(company, em) {
  let done = false;
  while (!done) {
    const invoice = await getNextInvoice(company.taxnumber, company.lastInvoice);
    if ( invoice ) {
      DI.invoiceRepository.persist(invoice);
      company.lastInvoice = invoice.id;
      em.flush();
    } else {
      done = true;
    }
  }
}

Does the DI.invoiceRepository.persist() in fetchInvoicesOfACompany() use the right EM instance? If not, what should I do?

Also, if I'm not mistaken, the em.flush() in fetchInvoicesOfACompany() does not update company, since that belongs to another EM - how should I handle situations like this?


Solution

  • First of all, repository is just a thin layer on top of EM (an extension point if you want), that bares the entity name so you don't have to pass it to the first parameter of EM method (e.g. em.find(Ent, ...) vs repo.find(...).

    Then the contexts - you need a dedicated context for each request, so it has its own identity map. If you use RequestContext helper, the context is created and saved via domain API. Thanks to this, all the methods that are executed inside the domain handler will use the right instance automatically - this happens in the em.getContext() method, that first checks the RequestContext helper.

    https://mikro-orm.io/docs/identity-map/#requestcontext-helper-for-di-containers

    Check the tests for better understanding of how it works:

    https://github.com/mikro-orm/mikro-orm/blob/master/tests/RequestContext.test.ts

    So if you use repositories, with RequestContext helper it will work just fine as the singleton repository instance will use the singleton EM instance that will then use the right request based instance via em.getContext() where approapriate.

    But if you use manual forking instead, you are responsible use the right repository instance - each EM fork will have its own one. So in this case you can't use a singleton, you need to do forkedEm.getRepository(Ent).

    Btw alternatively you can also use AsyncLocalStorage which is faster (and not deprecated), if you are on node 12+. The RequestContext helper implementation will use ALS in v5, as node 12+ will be requried.

    https://mikro-orm.io/docs/async-local-storage

    Another thing you could do is to use the RequestContext helper manually instead of via middlewares - something like the following:

    (async () => {
      DI.orm = await MikroORM.init();
      DI.em = DI.orm.em;
      DI.companyRepository = DI.orm.em.getRepository(Company);
      DI.invoiceRepository = DI.orm.em.getRepository(Invoice);
    ...
      await RequestContext.createAsync(DI.em, async () => {
        await fetchInvoices();
      })
    });
    
    async function fetchInvoices() {
      for (const company of await DI.companyRepository.findAll()) {
        await fetchInvoicesOfACompany(company)
      }
    }
    
    async function fetchInvoicesOfACompany(company) {
      let done = false;
    
      while (!done) {
        const invoice = await getNextInvoice(company.taxnumber, company.lastInvoice);
        
        if (invoice) {
          company.lastInvoice = invoice; // passing entity instance, no need to persist as `company` is managed entity and this change will be cascaded
          await DI.em.flush();
        } else {
          done = true;
        }
      }
    }