NestJS Logo

MikroORM

这个指南旨在帮助用户在Nest中快速入门MikroORM。MikroORM是基于数据映射器、工作单元和标识映射模式的Node.js的TypeScript ORM。它是TypeORM的一个很好的替代品,从TypeORM迁移过来应该相当容易。您可以在这里找到MikroORM的完整文档。

提示@mikro-orm/nestjs是一个第三方包,不由NestJS核心团队管理。请在适当的存储库中报告发现的任何问题。

安装#

将MikroORM与Nest集成的最简单方法是使用@mikro-orm/nestjs模块。 只需在Nest、MikroORM和底层驱动程序旁边安装它即可:


$ npm i @mikro-orm/core @mikro-orm/nestjs @mikro-orm/mysql # for mysql/mariadb

MikroORM还支持postgressqlitemongo等数据库。请查看官方文档以获取所有驱动程序的详细信息。

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


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

forRoot()方法接受与MikroORM包中的init()方法相同的配置对象。请查看此页面以获取完整的配置文档。

或者,我们可以通过创建一个名为mikro-orm.config.ts的配置文件来配置CLI,然后调用不带任何参数的forRoot()方法。但是,当您使用使用摇树优化的构建工具时,这将无法正常工作。


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

然后,整个项目中将可以注入EntityManager(无需在其他地方导入任何模块)。


import { MikroORM } from '@mikro-orm/core';
// Import EntityManager from your driver package or `@mikro-orm/knex`
import { EntityManager } from '@mikro-orm/mysql';

@Injectable()
export class MyService {
  constructor(
    private readonly orm: MikroORM,
    private readonly em: EntityManager,
  ) {}
}
注意 请注意,EntityManager是从@mikro-orm/driver包中导入的,其中的驱动可以是mysqlsqlitepostgres或您使用的其他驱动。如果您已经将@mikro-orm/knex安装为依赖项,您也可以从那里导入EntityManager

仓库#

MikroORM支持仓储设计模式。对于每个实体,我们可以创建一个仓储(repository)。请在此处阅读有关仓储的完整文档。要定义应在当前范围内注册哪些仓储,您可以使用forFeature()方法。例如,像这样使用:

注意不应该 通过forFeature()注册基本实体,因为它们没有对应的仓储。另一方面,基本实体需要在forRoot()(或通用的ORM配置)的列表中包含。

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

并将其导入到根AppModule中:


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

通过使用@InjectRepository()装饰器,我们可以将PhotoRepository注入到PhotoService中:


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

使用自定义仓库#

使用自定义仓储时,我们可以通过与getRepositoryToken()方法命名方式相同的方式来避免使用@InjectRepository()装饰器:


export const getRepositoryToken = <T>(entity: EntityName<T>) =>
  `${Utils.className(entity)}Repository`;

换句话说,只要我们将仓储的命名与实体的命名相同,并在名称后添加Repository后缀,该仓储将自动注册到Nest DI容器中。


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

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

由于自定义仓储的名称与getRepositoryToken()返回的名称相同,我们不再需要使用@InjectRepository()装饰器:


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

自动加载实体#

提醒autoLoadEntities选项是在v4.1.0中添加的。

手动将实体添加到连接选项的实体数组中可能很繁琐。此外,从根模块引用实体会打破应用程序的领域边界,并导致实现细节泄漏给应用程序的其他部分。为了解决这个问题,可以使用静态的 glob 路径。

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


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

在指定了该选项后,通过forFeature()方法注册的每个实体将自动添加到配置对象的实体数组中。

注意 没有通过forFeature()方法注册,而只是通过实体(通过关联)引用的实体将不会通过autoLoadEntities设置添加进来。
注意 使用autoLoadEntities选项对MikroORM CLI没有影响 - 对于CLI,我们仍然需要包含完整实体列表的CLI配置。另一方面,我们可以在CLI中使用glob路径,因为CLI不会经过webpack处理。

序列化#

提醒 MikroORM在每个实体关系中包装了一个Reference<T>Collection<T>对象,以提供更好的类型安全性。这将导致Nest内置的序列化器无法识别任何被包装的关系。换句话说,如果您从HTTP或WebSocket处理程序中返回MikroORM实体,它们的所有关系将不会被序列化。

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


@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;
}

队列中的请求范围处理程序#

注意@UseRequestContext()装饰器是在v4.1.0中添加的。

文档中所述,我们需要为每个请求保持干净的状态。这是通过通过中间件注册的RequestContext辅助程序自动处理的。

但是中间件只会在常规的HTTP请求处理过程中执行,如果我们需要在之外使用一个请求范围的方法呢?其中一种情况是队列处理程序或定时任务。

我们可以使用@UseRequestContext()装饰器。它要求您首先将MikroORM实例注入到当前上下文中,然后将用它来为您创建上下文。在内部,装饰器会为您的方法注册新的请求上下文,并在该上下文中执行方法。


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

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

使用 AsyncLocalStorage 进行请求上下文#

默认情况下,在RequestContext辅助程序中使用的是domain API。自@mikro-orm/core@4.0.3版本以来,如果您使用的是最新的Node.js版本,还可以使用新的AsyncLocalStorage:


// create new (global) storage instance
const storage = new AsyncLocalStorage<EntityManager>();

@Module({
  imports: [
    MikroOrmModule.forRoot({
      // ...
      registerRequestContext: false, // disable automatic middleware
      context: () => storage.getStore(), // use our AsyncLocalStorage instance
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// register the request context middleware
const app = await NestFactory.create(AppModule, { ... });
const orm = app.get(MikroORM);

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

测试#

@mikro-orm/nestjs包提供了getRepositoryToken()函数,该函数根据给定的实体返回准备好的令牌,以便模拟存储库。


@Module({
  providers: [
    PhotoService,
    {
      provide: getRepositoryToken(Photo),
      useValue: mockedRepository,
    },
  ],
})
export class PhotoModule {}

示例#

您可以在这里找到一个使用NestJS和MikroORM的真实世界示例。

支持一下