使用 MikroORM 和 NestJS 框架
安装
¥Installation
将 MikroORM 集成到 Nest 的最简单方法是通过 @mikro-orm/nestjs
模块。只需导入工厂,实例化它并调用 方法。
¥Easiest way to integrate MikroORM to Nest is via @mikro-orm/nestjs
module. Simply install it next to Nest, MikroORM and underlying driver:
$ yarn add @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mongodb # for mongo
$ yarn add @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mysql # for mysql/mariadb
$ yarn add @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mariadb # for mysql/mariadb
$ yarn add @mikro-orm/core @mikro-orm/nestjs @mikro-orm/postgresql # for postgresql
$ yarn add @mikro-orm/core @mikro-orm/nestjs @mikro-orm/sqlite # for sqlite
or
$ npm i -s @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mongodb # for mongo
$ npm i -s @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mysql # for mysql/mariadb
$ npm i -s @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mariadb # for mysql/mariadb
$ npm i -s @mikro-orm/core @mikro-orm/nestjs @mikro-orm/postgresql # for postgresql
$ npm i -s @mikro-orm/core @mikro-orm/nestjs @mikro-orm/sqlite # for sqlite
安装过程完成后,我们可以将 MikroOrmModule
导入根 AppModule
。
¥Once the installation process is completed, we can import the MikroOrmModule
into the root AppModule
.
@Module({
imports: [
MikroOrmModule.forRoot({
entities: ['./dist/entities'],
entitiesTs: ['./src/entities'],
dbName: 'my-db-name.sqlite3',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
forRoot()
方法从 MikroORM 包中接受与 init()
相同的配置对象。查看 此页面 以获取完整的配置文档。
¥The forRoot()
method accepts the same configuration object as init()
from the MikroORM package. Check this page for the complete configuration documentation.
或者,我们可以通过创建配置文件 mikro-orm.config.ts
来 配置 CLI,然后调用 forRoot()
而不使用任何参数。当你使用使用树摇动的构建工具时,这将不起作用。
¥Alternatively we can configure the CLI by creating a configuration file mikro-orm.config.ts
and then call the forRoot()
without any arguments. This won't work when you use a build tools that use tree shaking.
@Module({
imports: [
MikroOrmModule.forRoot(),
],
...
})
export class AppModule {}
之后,EntityManager
将可用于注入整个项目(无需在其他地方导入任何模块)。
¥Afterward, the EntityManager
will be available to inject across entire project (without importing any module elsewhere).
import { MikroORM } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/mysql'; // Import EntityManager from your driver package or `@mikro-orm/knex`
@Injectable()
export class MyService {
constructor(private readonly orm: MikroORM,
private readonly em: EntityManager) {
}
}
请注意,
EntityManager
是从@mikro-orm/driver
包导入的,其中驱动程序是mysql
、sqlite
、postgres
或你正在使用的任何驱动程序。¥Notice that the
EntityManager
is imported from the@mikro-orm/driver
package, where driver ismysql
,sqlite
,postgres
or whatever driver you are using.如果你已将
@mikro-orm/knex
安装为依赖,你也可以从那里导入EntityManager
。¥In case you have
@mikro-orm/knex
installed as a dependency, you can also import theEntityManager
from there.
存储库
¥Repositories
MikroORM 支持存储库设计模式。对于每个实体,我们都可以创建一个存储库。阅读完整的 此处的存储库文档。要定义哪些存储库应在当前范围内注册,你可以使用 forFeature()
方法。例如,以这种方式:
¥MikroORM supports the repository design pattern. For every entity we can create a repository. Read the complete documentation on repositories here. To define which repositories shall be registered in the current scope you can use the forFeature()
method. For example, in this way:
You should not 通过
forFeature()
注册你的基本实体,因为没有这些实体的存储库。另一方面,基本实体需要成为forRoot()
中列表的一部分(或通常在 ORM 配置中)。¥register your base entities via
forFeature()
, as there are no repositories for those. On the other hand, base entities need to be part of the list inforRoot()
(or in the ORM config in general).
// photo.module.ts
@Module({
imports: [MikroOrmModule.forFeature([Photo])],
providers: [PhotoService],
controllers: [PhotoController],
})
export class PhotoModule {}
重新生成实体并将其导入根 AppModule
:
¥and import it into the root AppModule
:
// app.module.ts
@Module({
imports: [MikroOrmModule.forRoot(...), PhotoModule],
})
export class AppModule {}
通过这种方式,我们可以使用 @InjectRepository()
装饰器将 PhotoRepository
注入到 PhotoService
:
¥In this way we can inject the PhotoRepository
to the PhotoService
using the @InjectRepository()
decorator:
@Injectable()
export class PhotoService {
constructor(
@InjectRepository(Photo)
private readonly photoRepository: EntityRepository<Photo>
) {}
// ...
}
使用自定义存储库
¥Using custom repositories
使用自定义存储库时,我们可以通过将存储库命名为与 getRepositoryToken()
方法相同的方式来解决对 @InjectRepository()
装饰器的需求:
¥When using custom repositories, we can get around the need for @InjectRepository()
decorator by naming our repositories the same way as getRepositoryToken()
method do:
export const getRepositoryToken = <T> (entity: EntityName<T>) => `${Utils.className(entity)}Repository`;
换句话说,只要我们将存储库命名为与实体相同的名称,并附加 Repository
后缀,存储库就会自动在 Nest.js DI 容器中注册。
¥In other words, as long as we name the repository same was as the entity is called, appending Repository
suffix, the repository will be registered automatically in the Nest.js DI container.
**./author.entity.ts**
@Entity({ repository: () => AuthorRepository })
export class Author {
// to allow inference in `em.getRepository()`
[EntityRepositoryType]?: AuthorRepository;
}
**./author.repository.ts**
import { EntityRepository } from '@mikro-orm/mysql'; // Import EntityManager from your driver package or `@mikro-orm/knex`
export class AuthorRepository extends EntityRepository<Author> {
// your custom methods...
}
由于自定义存储库名称与 getRepositoryToken()
返回的名称相同,因此我们不再需要 @InjectRepository()
装饰器:
¥As the custom repository name is the same as what getRepositoryToken()
would return, we do not need the @InjectRepository()
decorator anymore:
@Injectable()
export class MyService {
constructor(private readonly repo: AuthorRepository) { }
}
自动加载实体
¥Load entities automatically
autoLoadEntities
选项是在 v4.1.0 中添加的¥
autoLoadEntities
option was added in v4.1.0
手动将实体添加到连接选项的实体数组中可能很繁琐。此外,从根模块引用实体会破坏应用域边界,并导致实现细节泄露到应用的其他部分。要解决此问题,可以使用静态 glob 路径。
¥Manually adding entities to the entities array of the connection options can be tedious. In addition, referencing entities from the root module breaks application domain boundaries and causes leaking implementation details to other parts of the application. To solve this issue, static glob paths can be used.
但请注意,webpack 不支持 glob 路径,因此如果你在 monorepo 中构建应用,则无法使用它们。为了解决这个问题,提供了另一种解决方案。要自动加载实体,请将配置对象(传递到 forRoot()
方法中)的 autoLoadEntities
属性设置为 true
,如下所示:
¥Note, however, that glob paths are not supported by webpack, so if you are building your application within a monorepo, you won't be able to use them. To address this issue, an alternative solution is provided. To automatically load entities, set the autoLoadEntities
property of the configuration object (passed into the forRoot()
method) to true
, as shown below:
@Module({
imports: [
MikroOrmModule.forRoot({
// ...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
指定该选项后,通过 forFeature()
方法注册的每个实体都将自动添加到配置对象的实体数组中。
¥With that option specified, every entity registered through the forFeature()
method will be automatically added to the entities array of the configuration object.
请注意,未通过
forFeature()
方法注册但仅从实体(通过关系)引用的实体不会通过autoLoadEntities
设置包含在内。¥Note that entities that aren't registered through the
forFeature()
method, but are only referenced from the entity (via a relationship), won't be included by way of theautoLoadEntities
setting.
使用
autoLoadEntities
对 MikroORM CLI 也没有影响 - 为此,我们仍然需要具有完整实体列表的 CLI 配置。另一方面,我们可以在那里使用 glob,因为 CLI 不会通过 webpack。¥Using
autoLoadEntities
also has no effect on the MikroORM CLI - for that we still need CLI config with the full list of entities. On the other hand, we can use globs there, as the CLI won't go through webpack.
队列中的请求范围处理程序
¥Request scoped handlers in queues
@CreateRequestContext()
装饰器在@mikro-orm/core
包中可用。¥
@CreateRequestContext()
decorator is available in@mikro-orm/core
package.
在 v6 之前,
@CreateRequestContext()
被称为@UseRequestContext()
。¥Before v6,
@CreateRequestContext()
was called@UseRequestContext()
.
正如 docs 中提到的,我们需要每个请求都有一个干净的状态。这要归功于通过中间件注册的 RequestContext
助手,可以自动处理。
¥As mentioned in the docs, we need a clean state for each request. That is handled automatically thanks to the RequestContext
helper registered via middleware.
但是中间件仅针对常规 HTTP 请求句柄执行,如果我们需要除此之外的请求范围方法怎么办?其中一个例子是队列处理程序或计划任务。
¥But middlewares are executed only for regular HTTP request handles, what if we need a request scoped method outside of that? One example of that is queue handlers or scheduled tasks.
我们可以使用 @CreateRequestContext()
装饰器。它要求你首先将 MikroORM
实例注入当前上下文,然后它将用于为你创建上下文。在底层,装饰器将为你的方法注册新的请求上下文并在上下文中执行它。
¥We can use the @CreateRequestContext()
decorator. It requires you to first inject the MikroORM
instance to current context, it will be then used to create the context for you. Under the hood, the decorator will register new request context for your method and execute it inside the context.
@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.
@Controller()
export class MyService {
constructor(private readonly orm: MikroORM) { }
@CreateRequestContext()
async doSomething() {
// this will be executed in a separate context
}
}
或者,你可以提供一个回调,它将返回 MikroORM
实例。
¥Alternatively you can provide a callback that will return the MikroORM
instance.
import { DI } from '..';
export class MyService {
@CreateRequestContext(() => DI.orm)
async doSomething() {
// this will be executed in a separate context
}
}
另一件需要注意的事情是将它们与其他装饰器结合起来。例如,如果你将它与 NestJS 的“BullJS 队列模块”结合使用,则安全的做法是提取需要干净 docs 的代码部分,无论是在新方法中还是注入单独的服务。
¥Another thing to look out for how you combine them with other decorators. For example if you use it in combination with NestJS's "BullJS queues module", a safe bet is to extract the part of the code that needs a clean docs, either in a new method or inject a separate service.
@Processor({
name: 'example-queue',
})
export class MyConsumer {
constructor(private readonly orm: MikroORM) { }
@Process()
async doSomething(job: Job<any>) {
await this.doSomethingWithMikro();
}
@CreateRequestContext()
async doSomethingWithMikro() {
// this will be executed in a separate context
}
}
在这种情况下,@Process()
装饰器希望接收一个可执行函数,但如果我们也将 @CreateRequestContext()
添加到处理程序中,如果 @CreateRequestContext()
在 @Process()
之前执行,后者将接收 void
。
¥As in this case, the @Process()
decorator expects to receive an executable function, but if we add @CreateRequestContext()
to the handler as well, if @CreateRequestContext()
is executed before @Process()
, the later will receive void
.
@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.
使用 GraphQL 时的请求范围
¥Request scoping when using GraphQL
NestJS 中的 GraphQL 模块使用 apollo-server-express
,默认情况下启用 bodyparser
。(source)正如“DI 容器的 RequestContext 助手”中提到的,这会导致问题,因为 NestJS MikroORM 模块安装的中间件需要在 bodyparser
之后加载。同时,确保在 NestJs 本身中禁用 body-parser。
¥The GraphQL module in NestJS uses apollo-server-express
which enables bodyparser
by default. (source) As mentioned in "RequestContext helper for DI containers" this causes issues as the Middleware the NestJS MikroORM module installs needs to be loaded after bodyparser
. At the same time make sure to disable body-parser in NestJs itself as well.
这可以通过将 bodyparser 添加到 main.ts 文件中来完成
¥This can be done by adding bodyparser to your main.ts file
import { NestFactory } from '@nestjs/core';
import express from 'express';
async function bootstrap() {
const app = await NestFactory.create(AppModule,{ bodyParser: false });
app.use(express.json());
await app.listen(5555);
}
同时禁用 GraphQL 模块中的 bodyparser
¥And at the same time disabling the bodyparser in the GraphQL Module
@Module({
imports: [
GraphQLModule.forRoot({
bodyParserConfig: false,
}),
],
})
应用关闭和清理
¥App shutdown and cleanup
默认情况下,NestJS 不监听系统进程终止信号(例如 SIGTERM)。因此,如果进程终止,MikroORM 关闭逻辑将永远不会执行,这可能导致数据库连接保持打开状态并消耗资源。要启用此功能,需要在启动应用时调用 enableShutdownHooks
函数。
¥By default, NestJS does not listen for system process termination signals (for example SIGTERM). Because of this, the MikroORM shutdown logic will never execute if the process is terminated, which could lead to database connections remaining open and consuming resources. To enable this, the enableShutdownHooks
function needs to be called when starting up the application.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Starts listening for shutdown hooks
app.enableShutdownHooks();
await app.listen(3000);
}
有关 enableShutdownHooks 的更多信息
¥More information about enableShutdownHooks
多个数据库连接
¥Multiple Database Connections
你可以通过注册多个 MikroOrmModule
并设置其 contextName
来定义多个数据库连接。如果你想使用中间件请求上下文,则必须禁用自动中间件并使用 forMiddleware()
注册 MikroOrmModule
或使用 NestJS Injection Scope
¥You can define multiple database connections by registering multiple MikroOrmModule
and setting their contextName
. If you want to use middleware request context you must disable automatic middleware and register MikroOrmModule
with forMiddleware()
or use NestJS Injection Scope
@Module({
imports: [
MikroOrmModule.forRoot({
contextName: 'db1',
registerRequestContext: false, // disable automatatic middleware
...
}),
MikroOrmModule.forRoot({
contextName: 'db2',
registerRequestContext: false, // disable automatatic middleware
...
}),
MikroOrmModule.forMiddleware()
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
要访问不同的 MikroORM
/EntityManager
连接,你必须使用新的注入令牌 @InjectMikroORM()
/@InjectEntityManager()
,其中你需要传递 contextName
:
¥To access different MikroORM
/EntityManager
connections you have to use the new injection tokens @InjectMikroORM()
/@InjectEntityManager()
where you are required to pass the contextName
in:
@Injectable()
export class MyService {
constructor(@InjectMikroORM('db1') private readonly orm1: MikroORM,
@InjectMikroORM('db2') private readonly orm2: MikroORM,
@InjectEntityManager('db1') private readonly em1: EntityManager,
@InjectEntityManager('db2') private readonly em2: EntityManager) { }
}
使用 forFeature()
方法定义存储库时,你需要设置要针对哪个 contextName
注册:
¥When defining your repositories with forFeature()
method you will need to set which contextName
you want it registered against:
// photo.module.ts
@Module({
imports: [MikroOrmModule.forFeature([Photo], 'db1')],
providers: [PhotoService],
controllers: [PhotoController],
})
export class PhotoModule {}
使用 @InjectRepository
装饰器时,你还需要传递要从中获取它的 contextName
:
¥When using the @InjectRepository
decorator you will also need to pass the contextName
you want to get it from:
@Injectable()
export class PhotoService {
constructor(
@InjectRepository(Photo, 'db1')
private readonly photoRepository: EntityRepository<Photo>
) {}
// ...
}
序列化警告
¥Serialization caveat
NestJS 内置序列化 依赖于 class-transformer。从 MikroORM 3.2 开始,我们可以使用 Reference
助手来定义自己的实体而无需装饰器,这也适用于 Vanilla JavaScript。换句话说,如果你从 HTTP 或 WebSocket 处理程序返回 MikroORM 实体,则它们的所有关系都不会被序列化。
¥NestJS built-in serialization relies on class-transformer. Since MikroORM wraps every single entity relation in a Reference
or a Collection
instance (for type-safety), this will make the built-in ClassSerializerInterceptor
blind to any wrapped relations. In other words, if you return MikroORM entities from your HTTP or WebSocket handlers, all of their relations will NOT be serialized.
幸运的是,MikroORM 提供了一个 序列化 API,可以代替 ClassSerializerInterceptor
使用。
¥Luckily, MikroORM provides a serialization API which can be used in lieu of ClassSerializerInterceptor
.
@Entity()
export class Book {
@Property({ hidden: true }) // --> Equivalent of class-transformer's `@Exclude`
hiddenField: number = Date.now();
@Property({ persist: false }) // --> Will only exist in memory (and will be serialized). Similar to class-transformer's `@Expose()`
count?: number;
@ManyToOne({ serializer: value => value.name, serializedName: 'authorName' }) // Equivalent of class-transformer's `@Transform()`
author: Author;
}
测试
¥Testing
@mikro-orm/nestjs
包公开了 getRepositoryToken()
函数,该函数根据给定的实体返回准备好的令牌以允许模拟存储库。
¥The @mikro-orm/nestjs
package exposes getRepositoryToken()
function that returns prepared token based on a given entity to allow mocking the repository.
如果我们仅通过
getRepositoryToken()
注册提供程序,则需要使用@InjectRepository
装饰器。为了能够在没有此装饰器的情况下使用自定义存储库,我们需要将其注册到provide: PhotoRepository
。provide
:¥If we register the provider only via
getRepositoryToken()
, we need to use the@InjectRepository
decorator. To be able to use custom repository without this decorator, we need to register it withprovide: PhotoRepository
.provide
:
@Module({
providers: [
PhotoService,
// required for `@InjectRepository` decorator
{
provide: getRepositoryToken(Photo),
useValue: mockedRepository,
},
// required for custom repositories if we don't want to use `@InjectRepository`
{
provide: PhotoRepository,
useValue: mockedRepository,
},
],
})
export class PhotoModule {}
使用 EventSubscriber
¥Using EventSubscriber
订阅者通常仅通过 ORM 配置注册,但你也可以通过 EventManager.registerSubscriber()
动态注册。如果你希望订阅者使用 Nest.js DI 容器中的一些依赖,请使用 @Injectable
装饰器并在构造函数中手动注册订阅者,而不是将其传递给 ORM 配置:
¥Subscribers are normally registered only via the ORM config, but you can do so also dynamically via EventManager.registerSubscriber()
. If you want your subscriber to use some dependencies from the Nest.js DI container, use the @Injectable
decorator and register the subscriber manually in the constructor instead of passing it to the ORM config:
import { Injectable } from '@nestjs/common';
import { EntityName, EventArgs, EventSubscriber } from '@mikro-orm/core';
@Injectable()
export class AuthorSubscriber implements EventSubscriber<Author> {
constructor(em: EntityManager) {
em.getEventManager().registerSubscriber(this);
}
getSubscribedEntities(): EntityName<Author>[] {
return [Author];
}
async afterCreate(args: EventArgs<Author>): Promise<void> {
// ...
}
async afterUpdate(args: EventArgs<Author>): Promise<void> {
// ...
}
}
GraphQL 解析器
¥GraphQL resolvers
MikroORM 自 v6 起原生支持 dataloaders 的 Reference.load()
和 Collection.load()
方法。
¥MikroORM supports dataloaders natively since v6 for Reference.load()
and Collection.load()
methods.
@Resolver(() => Book)
class BookResolver {
@ResolveField(() => Author)
async author(@Parent() book: Book) {
return book.author.load({ dataloader: true }); // can be also enabled globally
}
}
示例
¥Example
可以找到 NestJS 与 MikroORM 的真实示例 此处
¥A real world example of NestJS with MikroORM can be found here