Skip to content

MikroORM

这篇秘籍旨在帮助用户在 Nest 中快速上手 MikroORM。MikroORM 是一个面向 Node.js 的 TypeScript ORM,基于 Data Mapper、Unit of Work 和 Identity Map 模式。它是 TypeORM 的一个优秀替代方案,而且从 TypeORM 迁移到它通常也比较容易。MikroORM 的完整文档可见这里

提示

@mikro-orm/nestjs 是第三方包,并不由 NestJS 核心团队维护。如果你在该库中发现问题,请在其对应仓库中反馈。

安装

将 MikroORM 集成到 Nest 中最简单的方式,是使用 @mikro-orm/nestjs 模块。 只需将它与 Nest、MikroORM 以及底层驱动一并安装即可:

bash
$ npm i @mikro-orm/core @mikro-orm/nestjs @mikro-orm/sqlite

MikroORM 也支持 postgressqlitemongo。所有驱动请参见官方文档

安装完成后,我们就可以将 MikroOrmModule 导入根 AppModule

typescript
import { SqliteDriver } from '@mikro-orm/sqlite';

@Module({
  imports: [
    MikroOrmModule.forRoot({
      entities: ['./dist/entities'],
      entitiesTs: ['./src/entities'],
      dbName: 'my-db-name.sqlite3',
      driver: SqliteDriver,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

forRoot() 方法接收与 MikroORM 包中 init() 相同的配置对象。完整配置文档请查看这一页

另外,你也可以通过创建一个 mikro-orm.config.ts 配置文件来配置 CLI,然后无参调用 forRoot()

typescript
@Module({
  imports: [
    MikroOrmModule.forRoot(),
  ],
  ...
})
export class AppModule {}

不过,如果你使用了带 tree shaking 的构建工具,这种方式就不会生效;此时更好的做法是显式传入配置:

typescript
import config from './mikro-orm.config'; // your ORM config

@Module({
  imports: [
    MikroOrmModule.forRoot(config),
  ],
  ...
})
export class AppModule {}

之后,EntityManager 就可以在整个项目中直接注入使用了(无需在其他地方额外导入模块)。

ts
// Import everything from your driver package or `@mikro-orm/knex`
import { EntityManager, MikroORM } from '@mikro-orm/sqlite';

@Injectable()
export class MyService {
  constructor(
    private readonly orm: MikroORM,
    private readonly em: EntityManager,
  ) {}
}

提示

请注意,EntityManager 是从 @mikro-orm/driver 包中导入的,其中 driver 可能是 mysqlsqlitepostgres 或你实际使用的驱动。如果你安装了 @mikro-orm/knex,也可以从那里导入 EntityManager

Repository

MikroORM 支持 repository 设计模式。每个实体都可以有一个 repository。完整 repository 文档请见这里。要定义当前作用域中应注册哪些 repository,可以使用 forFeature() 方法。例如:

提示

不应通过 forFeature() 注册基础实体(base entities),因为它们没有对应的 repository。 另一方面,基础实体仍然需要出现在 forRoot() 的列表中(或者更一般地,在 ORM 配置中)。

typescript
// photo.module.ts
@Module({
  imports: [MikroOrmModule.forFeature([Photo])],
  providers: [PhotoService],
  controllers: [PhotoController],
})
export class PhotoModule {}

然后把它导入根 AppModule

typescript
// app.module.ts
@Module({
  imports: [MikroOrmModule.forRoot(...), PhotoModule],
})
export class AppModule {}

这样一来,我们就可以在 PhotoService 中通过 @InjectRepository() 装饰器注入 PhotoRepository

typescript
@Injectable()
export class PhotoService {
  constructor(
    @InjectRepository(Photo)
    private readonly photoRepository: EntityRepository<Photo>,
  ) {}
}

使用自定义 repository

使用自定义 repository 时,我们就不再需要 @InjectRepository() 装饰器了,因为 Nest DI 会基于类引用来完成解析。

ts
// `**./author.entity.ts**`
@Entity({ repository: () => AuthorRepository })
export class Author {
  // to allow inference in `em.getRepository()`
  [EntityRepositoryType]?: AuthorRepository;
}

// `**./author.repository.ts**`
export class AuthorRepository extends EntityRepository<Author> {
  // your custom methods...
}

由于自定义 repository 的名称与 getRepositoryToken() 返回的名称一致,因此不再需要 @InjectRepository() 装饰器:

ts
@Injectable()
export class MyService {
  constructor(private readonly repo: AuthorRepository) {}
}

自动加载实体

手动将实体添加到连接配置中的 entities 数组可能会很繁琐。此外,在根模块中直接引用实体会破坏应用的领域边界,并将实现细节泄露到系统其他部分。为了解决这个问题,可以使用静态 glob 路径。

不过需要注意,webpack 不支持 glob 路径,因此如果你是在 monorepo 中构建应用,就无法使用这种方式。为了解决这个问题,这里提供了另一种方案。要自动加载实体,请将传给 forRoot() 方法的配置对象中的 autoLoadEntities 属性设置为 true,如下所示:

ts
@Module({
  imports: [
    MikroOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

设置该选项后,所有通过 forFeature() 方法注册的实体,都会自动加入配置对象的 entities 数组中。

提示

请注意,那些没有通过 forFeature() 注册,而只是通过实体关系被引用到的实体,不会因 autoLoadEntities 设置而被包含进来。

提示

autoLoadEntities 对 MikroORM CLI 也没有影响。对于 CLI,我们仍然需要提供包含完整实体列表的 CLI 配置。不过 CLI 可以使用 glob,因为它不会经过 webpack。

序列化

注意

MikroORM 会将每一个实体关系都包装为 Reference<T>Collection<T> 对象,以提供更强的类型安全。这会导致 Nest 内置序列化器 无法识别这些被包装的关系。换句话说,如果你在 HTTP 或 WebSocket 处理器中直接返回 MikroORM 实体,它们的关系字段将不会被序列化。

幸运的是,MikroORM 提供了一个序列化 API,可以替代 ClassSerializerInterceptor

typescript
@Entity()
export class Book {
  @Property({ hidden: true }) // Equivalent of class-transformer's `@Exclude`
  hiddenField = Date.now();

  @Property({ persist: false }) // Similar to class-transformer's `@Expose()`. Will only exist in memory, and will be serialized.
  count?: number;

  @ManyToOne({
    serializer: (value) => value.name,
    serializedName: 'authorName',
  }) // Equivalent of class-transformer's `@Transform()`
  author: Author;
}

队列中的请求作用域处理器

文档所述,我们需要为每个请求提供一个干净状态。通常这是通过 middleware 注册的 RequestContext 帮助器自动完成的。

但中间件只会在普通 HTTP 请求处理中执行。如果我们在这之外也需要请求作用域方法怎么办?一个典型例子就是队列处理器或定时任务。

我们可以使用 @CreateRequestContext() 装饰器。它要求你先把 MikroORM 实例注入到当前上下文中,随后它会利用这个实例为你创建上下文。在底层,这个装饰器会为你的方法注册一个新的 request context,并在该上下文中执行方法。

ts
@Injectable()
export class MyService {
  constructor(private readonly orm: MikroORM) {}

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

注意

正如名称所示,这个装饰器总是会创建一个新的上下文;而它的替代方案 @EnsureRequestContext 则只会在当前尚未处于其他上下文中时才创建。

测试

@mikro-orm/nestjs 包暴露了 getRepositoryToken() 函数,它会根据给定实体返回对应的 token,从而方便你在测试中 mock repository。

typescript
@Module({
  providers: [
    PhotoService,
    {
      // or when you have a custom repository: `provide: PhotoRepository`
      provide: getRepositoryToken(Photo),
      useValue: mockedRepository,
    },
  ],
})
export class PhotoModule {}

示例

一个基于 NestJS 与 MikroORM 的真实示例项目见这里

基于 NestJS 官方文档翻译