Skip to main content
Version: 6.4

身份映射和请求上下文

MikroORM 在后台使用身份映射,因此我们始终会获得一个实体的相同实例。

¥MikroORM uses identity map in background, so we will always get the same instance of one entity.

什么是 "身份映射"

¥What is an "Identity Map"

你可以将 "身份映射" 视为一种 "在内存缓存中",从某种意义上说,它从空开始,在你使用实体管理器执行调用时被填充和更新,并且当操作与身份映射知道的 ID 匹配时,其中的项目会被从中拉出("缓存命中" 之类的)。但是,它也不同于实际的(结果)缓存,不应将其用作缓存(有关实际结果缓存,请参阅 此处)。缓存通常旨在提高跨请求的性能。身份映射旨在通过使可以轻松比较实体对象成为可能来提高单个请求中的性能,这反过来又使 ORM 能够批量操作数据库。它还可以帮助减少应用每个请​​求的内存占用,确保即使你进行多个与同一行匹配的查询,这些行也只会在内存中存在一次。

¥You can think of an "identity map" as a sort of "in memory cache", in the sense that it starts off empty, gets filled and updated as you perform calls with the entity manager, and items in it get pulled out of it ("cache hit" of sorts) when an operation matches an ID the identity map is aware of. However, it is also different from an actual (result) cache, and should not be used as one (See here for an actual result cache). Caches are generally meant to improve performance across requests. An identity map is instead meant to improve performance within a single request, by making it possible to compare entity objects trivially, which in turn enables the ORM to batch operations to the database. It also helps to reduce your application's memory footprint per request, by ensuring that even if you make multiple queries that match the same rows, those rows will only exist once in memory.

例如:

¥For example:

const authorRepository = em.getRepository(Author);
const jon = await authorRepository.findOne({ name: 'Jon Snow' }, { populate: ['books'] });
const authors = await authorRepository.findAll({ populate: ['books'] });

// identity map in action
console.log(jon === authors[0]); // true

如果我们想清除这个身份映射缓存,我们可以通过 em.clear() 方法进行清除:

¥If we want to clear this identity map cache, we can do so via em.clear() method:

orm.em.clear();

我们应该始终为每个请求保留唯一的身份映射。这基本上意味着我们需要克隆实体管理器并在请求上下文中使用克隆。有两种方法可以实现这一点:

¥We should always keep unique identity map per each request. This basically means that we need to clone entity manager and use the clone in request context. There are two ways to achieve this:

分叉实体管理器

¥Forking Entity Manager

使用 fork() 方法,我们可以简单地获得具有自己的上下文和身份映射的干净实体管理器:

¥With fork() method we can simply get clean entity manager with its own context and identity map:

const em = orm.em.fork();

全局身份映射

¥Global Identity Map

自 v5 起,也可以使用多态可嵌入项。这是一个常见问题,导致奇怪的错误,因为在没有请求上下文的情况下使用全局 EM 几乎总是错误的,我们总是需要为每个请求设置一个专用的上下文,这样它们就不会干扰。

¥Since v5, it is no longer possible to use the global identity map. This was a common issue that led to weird bugs, as using the global EM without request context is almost always wrong, we always need to have a dedicated context for each request, so they do not interfere.

我们仍然可以通过 allowGlobalContext 配置或连接的环境变量 MIKRO_ORM_ALLOW_GLOBAL_CONTEXT 禁用此检查 - 这在单元测试中尤其方便。

¥We can still disable this check via allowGlobalContext configuration, or a connected environment variable MIKRO_ORM_ALLOW_GLOBAL_CONTEXT - this can be handy especially in unit tests.

RequestContext 助手

¥RequestContext helper

如果我们使用依赖注入容器(如 inversifynestjs 框架中的容器),则很难实现这一点,因为我们通常希望通过 DI 容器访问我们的存储库,但它总是会为我们提供相同的实例,而不是为每个请求提供新的实例。

¥If we use dependency injection container like inversify or the one in nestjs framework, it can be hard to achieve this, because we usually want to access our repositories via DI container, but it will always provide us with the same instance, rather than new one for each request.

为了解决这个问题,我们可以使用 RequestContext 助手,它将在后台使用 nodeAsyncLocalStorage 来隔离请求上下文。如果可用,MikroORM 将始终使用请求特定的(分叉的)实体管理器,因此我们需要做的就是创建新的请求上下文,最好是作为中间件:

¥To solve this, we can use RequestContext helper, that will use node's AsyncLocalStorage in the background to isolate the request context. MikroORM will always use request specific (forked) entity manager if available, so all we need to do is to create new request context preferably as a middleware:

// `orm.em` is the global EntityManager instance

app.use((req, res, next) => {
// calls `orm.em.fork()` and attaches it to the async context
RequestContext.create(orm.em, next);
});

app.get('/', async (req, res) => {
// uses fork from the async context automatically
const authors = await orm.em.find(Book, {});
res.json(authors);
});

我们应该将此中间件注册为请求处理程序之前和使用 ORM 的任何自定义中间件之前的最后一个中间件。当我们在请求处理中间件(如 queryParserbodyParser)之前注册它时可能会出现问题,因此一定要在它们之后注册上下文。

¥We should register this middleware as the last one just before request handlers and before any of our custom middleware that is using the ORM. There might be issues when we register it before request processing middleware like queryParser or bodyParser, so definitely register the context after them.

稍后我们可以通过 RequestContext.getEntityManager() 访问请求范围 EntityManager。此方法在后台自动使用,因此我们不需要它。

¥Later on we can then access the request scoped EntityManager via RequestContext.getEntityManager(). This method is used under the hood automatically, so we should not need it.

如果上下文尚未启动,RequestContext.getEntityManager() 将返回 undefined

¥RequestContext.getEntityManager() will return undefined if the context was not started yet.

RequestContext 助手如何工作?

¥How does RequestContext helper work?

在内部,所有与身份映射一起使用的 EntityManager 方法(例如 em.find()em.getReference())首先调用 em.getContext() 来访问上下文分支。此方法将首先检查我们是否在 RequestContext 处理程序中运行,并优先从中分叉 EntityManager

¥Internally all EntityManager methods that work with the Identity Map (e.g. em.find() or em.getReference()) first call em.getContext() to access the contextual fork. This method will first check if we are running inside RequestContext handler and prefer the EntityManager fork from it.

// we call em.find() on the global EM instance
const res = await orm.em.find(Book, {});

// but under the hood this resolves to
const res = await orm.em.getContext().find(Book, {});

// which then resolves to
const res = await RequestContext.getEntityManager().find(Book, {});

然后,RequestContext.getEntityManager() 方法检查我们在 RequestContext.create() 方法中用于创建新 EM 分叉的 AsyncLocalStorage 静态实例。

¥The RequestContext.getEntityManager() method then checks AsyncLocalStorage static instance we use for creating new EM forks in the RequestContext.create() method.

Node.js 核心中的 AsyncLocalStorage 类是这里的魔术师。它允许我们在整个异步调用过程中跟踪上下文。它允许我们通过全局 EntityManager 实例将 EntityManager 分支创建(通常在中间件中,如上一节所示)与其使用分离。

¥The AsyncLocalStorage class from Node.js core is the magician here. It allows us to track the context throughout the async calls. It allows us to decouple the EntityManager fork creation (usually in a middleware as shown in previous section) from its usage through the global EntityManager instance.

使用自定义 AsyncLocalStorage 实例

¥Using custom AsyncLocalStorage instance

RequestContext 助手拥有自己的 AsyncLocalStorage 实例,ORM 在解析 em.getContext() 时会自动检查该实例。如果你想要自带,可以使用 context 选项:

¥The RequestContext helper holds its own AsyncLocalStorage instance, which the ORM checks automatically when resolving em.getContext(). If you want to bring your own, you can do so by using the context option:

const storage = new AsyncLocalStorage<EntityManager>();

const orm = await MikroORM.init({
context: () => storage.getStore(),
// ...
});

app.use((req, res, next) => {
storage.run(orm.em.fork({ useContext: true }), next);
});

@CreateRequestContext() 装饰器

¥@CreateRequestContext() decorator

在 v6 之前,@CreateRequestContext() 被称为 @UseRequestContext()

¥Before v6, @CreateRequestContext() was called @UseRequestContext().

中间件仅针对常规 HTTP 请求处理程序执行,如果你需要除此之外的请求范围方法怎么办?其中一个例子是队列处理程序或计划任务(例如 CRON 作业)。

¥Middlewares are executed only for regular HTTP request handlers, what if you need a request scoped method outside that? One example of that is queue handlers or scheduled tasks (e.g. CRON jobs).

在这些情况下,你可以使用 @CreateRequestContext() 装饰器。它要求你首先将 MikroORM 实例(或 EntityManager 或某些 EntityRepository)注入当前上下文,然后它将用于为我们创建新上下文。在底层,装饰器将为我们的方法注册新的请求上下文并在其中执行该方法(通过 RequestContext.create())。

¥In those cases, you can use the @CreateRequestContext() decorator. It requires you to first inject the MikroORM instance (or an EntityManager or some EntityRepository) to current context, it will then be used to create a new context for us. Under the hood, the decorator will register the new request context for our method and execute the method inside it (via RequestContext.create()).

@CreateRequestContext() 应该只在顶层方法上使用。它不应该嵌套 - 用它修饰的方法不应调用另一个也用它修饰的方法。

¥@CreateRequestContext() should be used only on the top level methods. It should not be nested - a method decorated with it should not call another method that is also decorated with it.

export class MyService {

// or `private readonly em: EntityManager`
constructor(private readonly orm: MikroORM) { }

@CreateRequestContext()
async doSomething() {
// this will be executed in a separate context
}

}

或者,你可以提供一个回调,它将返回 MikroORMEntityManagerEntityRepository 实例之一。

¥Alternatively you can provide a callback that will return one of MikroORM, EntityManager or EntityRepository instance.

import { DI } from '..';

export class MyService {

@CreateRequestContext(() => DI.em)
async doSomething() {
// this will be executed in a separate context
}

}

回调将在第一个参数中接收当前 this。你也可以使用它来链接 EntityManagerEntityRepository

¥The callback will receive current this in the first parameter. You can use it to link the EntityManager or EntityRepository too:

export class MyService {

constructor(private readonly userRepository: EntityRepository<User>) { }

@CreateRequestContext<MyService>(t => t.userRepository)
async doSomething() {
// this will be executed in a separate context
}

}

@EnsureRequestContext() 装饰器

¥@EnsureRequestContext() decorator

有时你可能更愿意确保方法在请求上下文中执行,并重用现有上下文(如果可用)。你可以在此处使用 @EnsureRequestContext() 装饰器,它的行为与 @CreateRequestContext 完全相同,但仅在必要时创建新上下文,并尽可能重用现有上下文。

¥Sometimes you may prefer to just ensure the method is executed inside a request context, and reuse the existing context if available. You can use the @EnsureRequestContext() decorator here, it behaves exactly like the @CreateRequestContext, but only creates new context if necessary, reusing the existing one if possible.

@Transactional() 装饰器

¥@Transactional() decorator

如果你想确保该方法在事务范围内运行,请使用 @Transactional() 装饰器。

¥If you want to ensure that the method runs within a transaction scope, use @Transactional() decorator.

你可以通过 context 选项提供可在 @CreateRequestContext() 装饰器中使用的相同回调,从而允许你在非标准场景中处理 EntityManager 的注入。该函数在新的事务上下文中执行,完成后,将自动执行刷新和提交。此装饰器也可以嵌套使用。

¥You can provide the same callback that can be used in the @CreateRequestContext() decorator via the context option, allowing you to handle the injection of the EntityManager in non-standard scenarios. The function is executed within a new transaction context, and upon completion, flush and commit are automatically executed. This decorator can also be used in a nested manner.

有关事务的更多详细信息,请检查 事务部分

¥For more details about transactions, check the Transactions section.

为什么需要请求上下文?

¥Why is Request Context needed?

假设我们将在整个应用中使用单个身份映射。它将在所有可能并行运行的请求处理程序之间共享。

¥Imagine we will use a single Identity Map throughout our application. It will be shared across all request handlers, that can possibly run in parallel.

问题 1 - 内存占用增加

¥Problem 1 - growing memory footprint

由于只有一个共享身份映射,我们不能在请求结束后就清除它。可能还有另一个请求正在使用它,因此从一个请求中清除身份映射可能会破坏并行运行的其他请求。这将导致内存占用量增加,因为在某个时间点被管理的每个实体都将保留在身份映射中。

¥As there would be only one shared Identity Map, we can't just clear it after our request ends. There can be another request working with it so clearing the Identity Map from one request could break other requests running in parallel. This will result in growing memory footprint, as every entity that became managed at some point in time would be kept in the Identity Map.

问题 2 - API 端点响应不稳定

¥Problem 2 - unstable response of API endpoints

每个实体都有 toJSON() 方法,该方法会自动将其转换为序列化形式如果我们只有一个共享身份映射,则可能会发生以下情况:

¥Every entity has toJSON() method, that automatically converts it to serialized form If we have only one shared Identity Map, following situation may occur:

假设有 2 个端点

¥Let's say there are 2 endpoints

  1. GET /book/:id 仅返回书籍,不填充任何内容

    ¥GET /book/:id that returns just the book, without populating anything

  2. GET /book-with-author/:id 返回已填充的书籍及其作者

    ¥GET /book-with-author/:id that returns the book and its author populated

现在,当有人通过这两个端点请求同一本书时,我们最终可能会得到相同的输出:

¥Now when someone requests same book via both of those endpoints, we could end up with both returning the same output:

  1. GET /book/1 返回 Book,但不填充其属性 author 属性

    ¥GET /book/1 returns Book without populating its property author property

  2. GET /book-with-author/1 返回 Book,这次填充了 author

    ¥GET /book-with-author/1 returns Book, this time with author populated

  3. GET /book/1 返回 Book,但这次也填充了 author

    ¥GET /book/1 returns Book, but this time also with author populated

发生这种情况是因为有关正在填充的实体关联的信息存储在身份映射中。

¥This happens because the information about entity association being populated is stored in the Identity Map.