在 NestJS 中使用 TypeORM:实体、仓库、事务与多数据源

原文作者:NestJS 文档维护者;文档版权 Kamil Myśliwiec。中文译写与编辑整理:未完纪。

@nestjs/typeorm 把 TypeORM 的数据源、仓库和实体装配进 Nest 的模块与依赖注入体系。TypeORM 使用 TypeScript 编写,因此能够自然配合 Nest 的类型、装饰器和服务类。本文按 NestJS 官方 TypeORM 章节 完整整理,从基本连接一直覆盖事务、多数据库、测试和自定义数据源工厂。

本章以 MySQL 为例。TypeORM 也支持 PostgreSQL、Oracle、Microsoft SQL Server、SQLite,以及 MongoDB 等存储;切换数据库时需要相应客户端与配置,不能把 MySQL 驱动直接用于所有后端。下文是装配与持久化教程,不包含完整鉴权、控制器校验或可直接部署的项目。所有代码仅作静态审阅。

根模块通过forRoot或异步工厂注册DataSource;业务模块通过forFeature注册实体仓库,再由服务注入。DataSource与事务manager执行数据库操作,生产结构变更由迁移管理。
Nest 模块与 TypeORM 的装配关系(原创技术示意图)

连接数据库并注册根模块

先安装集成包、TypeORM 和 MySQL 客户端:

npm install --save @nestjs/typeorm typeorm mysql2

在根模块 app.module.ts 导入 TypeOrmModule。下面保留官方初始演示配置:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

不要在生产使用这组演示设置。 官方明确警告,synchronize: true 可能导致生产数据丢失;root/root 也只用于说明配置字段。真实部署应使用最小权限账号、受保护的秘密配置和经过审核的迁移。

forRoot() 接受 TypeORM DataSource 构造器支持的选项,Nest 集成层另外提供以下参数:

参数 作用与默认值
retryAttempts 连接尝试次数,默认10。
retryDelay 连接重试间隔,默认3000毫秒。
toRetry 接收连接错误并决定是否重试的函数;默认对所有错误重试。
verboseRetryLog 是否把错误消息纳入每次连接重试日志;默认false。
autoLoadEntities 自动加入通过forFeature注册的实体;默认false。
manualInitialization 默认false。设置true时,模块初始化不建立连接,也不运行迁移;需自行调用DataSource.initialize并处理所需重试。

装配完成后,默认数据源的 DataSource 和 EntityManager 可通过 Nest 注入,不必为每个服务再次导入连接模块。原文用下面的根模块说明 DataSource 注入;forRoot() 的配置仍需由项目提供,不应把空调用理解为连接设置凭空存在:

import { DataSource } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(private dataSource: DataSource) {}
}

JavaScript 版本没有 TypeScript 的构造器参数属性与类型信息,可以用 @Dependencies(DataSource) 显式声明依赖,并在构造器中赋值;原文对应的写法为:

import { Module, Dependencies } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Dependencies(DataSource)
@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }
}

每个实体都有自己的仓库

TypeORM 支持仓库模式:每种实体对应自己的 Repository,仓库由数据源提供。先在 users/user.entity.ts 定义 User:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

主键自动生成,名字和姓氏是普通列,isActive 的数据库默认值为 true。官方建议把实体放在所属领域模块附近,便于保持业务结构清晰;项目并非必须使用某个固定目录。严格 TypeScript 项目还需按其实体初始化约定处理属性初始化检查,这里保留文档声明形式。

让数据源知道这个实体,可以把它加入根连接的 entities;如果项目使用适用的静态 glob,或后文的自动加载方式,则按所选方式装配。下面对应原文第二个根配置:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity.js';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

entities 解决数据源的实体元数据问题,forFeature() 解决当前 Nest 模块能注入哪些仓库的问题,两者不能简单互相替代。在 users.module.ts 中注册:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service.js';
import { UsersController } from './users.controller.js';
import { User } from './user.entity.js';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

随后在 UsersService 构造器中使用 @InjectRepository(User):

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity.js';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

列表查询返回数组,单项查询可能得到 null,删除示例只等待数据库操作完成。它并没有定义“记录不存在应返回什么HTTP响应”等业务语义。还要把 UsersModule 导入根 AppModule。

JavaScript 版本使用仓库注入 token。以下保留原文的等价服务,关键区别在 @Dependencies(getRepositoryToken(User)):

import { Injectable, Dependencies } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity.js';

@Injectable()
@Dependencies(getRepositoryToken(User))
export class UsersService {
  constructor(usersRepository) {
    this.usersRepository = usersRepository;
  }

  findAll() {
    return this.usersRepository.find();
  }

  findOne(id) {
    return this.usersRepository.findOneBy({ id });
  }

  async remove(id) {
    await this.usersRepository.delete(id);
  }
}

让其他模块使用仓库

如果另一个模块也要注入这里注册的仓库,需要重新导出 forFeature() 生成的提供者。可以直接导出 TypeOrmModule:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity.js';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule],
})
export class UsersModule {}

这样 UserHttpModule 导入 UsersModule 后,它的提供者便能注入 User 仓库。原文用如下配置展示跨模块使用:

import { Module } from '@nestjs/common';
import { UsersModule } from './users.module.js';
import { UsersService } from './users.service.js';
import { UsersController } from './users.controller.js';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UserHttpModule {}

表达实体之间的关系

表关系通常建立在共享字段、主键与外键上。TypeORM 用装饰器描述三个基本类别:

关系 装饰器与含义
一对一 @OneToOne():对应的一条记录关联另一侧的一条记录。
一对多/多对一 @OneToMany()、@ManyToOne():一侧一条记录对应另一侧多条记录。
多对多 @ManyToMany():两侧都可关联对方多条记录。

例如一个用户可以有多张照片,在 User 上增加 photos 关系:

import {
  Entity, Column, PrimaryGeneratedColumn, OneToMany,
} from 'typeorm';
import { Photo } from '../photos/photo.entity.js';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}

这里的 Photo 及其 user 关系需要在项目中定义。原章用这个片段讲关系声明,并未给出完整照片模块。实际外键、加载方式与级联行为需按 TypeORM 关系文档 设计,不能仅凭装饰器名字推断所有业务约束已经满足。

自动加载实体,保持模块边界

把所有实体都写进根模块容易泄露领域细节,也会让根配置不断膨胀。可以在 forRoot 选项中启用 autoLoadEntities:

TypeOrmModule.forRoot({
  // 项目已有的数据库配置
  type: 'mysql',
  autoLoadEntities: true,
})

这是对原文省略号配置的等价片段,仍需填写完整连接选项。开启后,每个通过 forFeature() 注册的实体会自动加入数据源的实体集合。只被其他实体的关系属性引用、却没有通过 forFeature() 注册的实体,不会自动加入。 因此给 User 写了 photos: Photo[],并不保证 Photo 的元数据已经注册。

把实体定义与类分开

除了在类上使用装饰器,也可以通过 EntitySchema 单独描述列与关系。下面保留原文的完整定义形式:

import { EntitySchema } from 'typeorm';
import { User } from './user.entity.js';

export const UserSchema = new EntitySchema<User>({
  name: 'User',
  target: User,
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    firstName: { type: String },
    lastName: { type: String },
    isActive: {
      type: Boolean,
      default: true,
    },
  },
  relations: {
    photos: {
      type: 'one-to-many',
      target: 'Photo',
    },
  },
});

target: 'Photo' 指的是照片 schema 的名称。若提供 target 类,则 name 必须与目标类名一致;不提供 target 时,名称可以自定义。使用这一替代写法时还要确保类型中的关系属性和对端 schema 相互匹配。

Nest 接受实体类的位置通常也可以接收 EntitySchema 实例,因而模块注册改成:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema.js';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';

@Module({
  imports: [TypeOrmModule.forFeature([UserSchema])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

事务需要同时处理成功、失败与连接释放

事务把一组数据库操作视为一个工作单元。原文推荐在需要完全控制事务时使用 QueryRunner;首先注入 DataSource:

@Injectable()
export class UsersService {
  constructor(private dataSource: DataSource) {}
}

DataSource 从 typeorm 导入。原文随后给出如下事务片段;这里完整保留,以便说明它的控制流程与限制:

async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);
    await queryRunner.commitTransaction();
  } catch (err) {
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}

成功路径通过事务自身的 manager 保存数据并提交;失败时回滚;手动创建的 QueryRunner 需要释放。注意,本例只保存数组前两项,并不实现处理任意数量用户的完整 createMany。

静态审核发现两个需补足的边界:catch 回滚后未再次抛错,调用者可能把失败理解为正常完成;connect 和 startTransaction 在 try 外,初始化失败时没有经过这个 finally。实际实现应把资源生命周期放进受保护的流程,传播失败,并处理回滚或释放本身发生错误的情况。不能只复制这个片段就认定事务处理完备。

测试直接依赖完整 DataSource 的类,往往要模拟大量无关方法。原文因此建议引入较窄的工厂接口,例如只提供事务所需方法的 QueryRunnerFactory,使替身关注真正使用的能力。

另一个选择是使用数据源的回调式事务。原文对应示例为:

async createMany(users: User[]) {
  await this.dataSource.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

编辑补充:需要处理任意长度数组时,可在回调内遍历 users,始终通过传入的事务 manager 保存,或在合适的仓库语义下保存数组;不要在回调里转用与该事务无关的全局仓库。回调抛出的异常应由调用层按业务要求处理,不要吞掉后仍返回成功。

用订阅器监听实体事件

TypeORM subscriber 可以监听指定实体的事件。原文在构造器中把订阅器实例加入数据源:

import {
  DataSource,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { User } from './user.entity.js';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(dataSource: DataSource) {
    dataSource.subscribers.push(this);
  }

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log('BEFORE USER INSERTED: ', event.entity);
  }
}

listenTo() 限定监听 User,beforeInsert 在插入前得到事件对象。订阅器不能是请求作用域提供者。日志行是原文的调试演示,直接打印完整实体可能泄露姓名或后续增加的敏感字段;生产日志应限制字段并脱敏。

再把订阅器放进模块的 providers:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity.js';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';
import { UserSubscriber } from './user.subscriber.js';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserSubscriber],
  controllers: [UsersController],
})
export class UsersModule {}

通过迁移管理结构变更

迁移让数据库结构逐步跟上模型演进,并在设计上考虑保留既有数据。TypeORM 提供专用 CLI 生成、运行和回退迁移。迁移是否安全仍取决于实际变更、数据与操作顺序,不能把“使用迁移”当作自动无损保证。

迁移类由 TypeORM 管理生命周期,独立于 Nest 应用的装配过程。因此不能在迁移中直接依赖 Nest 的依赖注入或其他专属能力。原文将具体操作交给 TypeORM 迁移文档,本次不执行任何数据库命令。

注册并选择多个数据源

一个项目可能需要多个连接。除默认数据源外,每个数据源必须有唯一名称。原文假设 User 与 Album 分别位于自己的数据库:

const defaultOptions = {
  type: 'postgres' as const,
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      entities: [User],
    }),
    TypeOrmModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      entities: [Album],
    }),
  ],
})
export class AppModule {}

这组 PostgreSQL 配置需要对应驱动,主机名、凭据和同步设置仍是演示值。不设置名称时,数据源名为 default;多个数据源都不命名,或者使用同一个名字,会互相覆盖。

异步配置时,name 必须放在 useFactory、useClass 或 useExisting 的同级位置。下面只是展示名称位置的配置结构,工厂及依赖由项目填入:

TypeOrmModule.forRootAsync({
  name: 'albumsConnection',
  useFactory: createAlbumsOptions,
  inject: [ConfigService],
})

createAlbumsOptions 是这里代替原文省略号的占位名称,不是库自带函数。这个位置影响 Nest 在装配时为数据源注册的注入 token,不能仅把名称藏在工厂返回的连接选项中。

注册实体仓库时也要选择数据源。省略名称会使用 default:

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

相应的仓库注入使用 @InjectRepository(Album, 'albumsConnection')。数据源和实体管理器也可按名注入:

@Injectable()
export class AlbumsService {
  constructor(
    @InjectDataSource('albumsConnection')
    private dataSource: DataSource,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager,
  ) {}
}

若通过工厂 provider 构造服务,使用 getDataSourceToken() 取得目标数据源 token。下面是原文的另一种构造形式,它假设服务构造器在该版本中只接收一个数据源参数,不能与上面双参数构造器直接混用:

@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsConnection: DataSource) => {
        return new AlbumsService(albumsConnection);
      },
      inject: [getDataSourceToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

用仓库替身测试服务行为

单元测试通常希望避免真实数据库,让测试独立而快速。可以用自定义提供者替换仓库。每个仓库注册都有对应 token,getRepositoryToken(User) 负责返回它;命名数据源则把名称作为第二个参数。

@Module({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository,
    },
  ],
})
export class UsersModule {}

此后服务使用 @InjectRepository(User) 时,Nest 注入的是 mockRepository。替身需要实现测试中实际调用的方法,并按断言要求配置返回值。原章没有定义一套完整测试数据,也没有证明数据库行为;仓库替身不能验证 SQL、数据库约束、迁移、连接权限或真实事务。

把连接选项异步提供给模块

连接设置可以不在模块源码中写死。forRootAsync() 支持工厂函数、工厂类和已有工厂对象三种常见方式。最简单的是把原配置放到返回函数中:

TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [],
    synchronize: true,
  }),
});

工厂像其他 Nest 异步 provider 一样,可以是 async,也可以通过 inject 接收依赖。原文用 ConfigService 读取配置,下面对应其结构,但把生产危险的自动同步改为 false;这是明确的编辑调整,实际连接值须由项目验证:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: false,
  }),
  inject: [ConfigService],
});

一元加号用于把端口转成数字,但它不负责检查环境变量是否缺失或端口是否合法;需要在项目配置边界做验证。这里的 ConfigModule、ConfigService 应由项目提供,秘密值不应写入日志或提交进仓库。

useClass:在 TypeOrmModule 内实例化工厂

TypeOrmModule.forRootAsync({
  useClass: TypeOrmConfigService,
});

Nest 会实例化该类,并调用 createTypeOrmOptions()。类需实现 TypeOrmOptionsFactory。下面保留原例的完整工厂形式,凭据仍是本地演示值:

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    };
  }
}

useExisting:复用另一个模块导出的工厂

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

这与 useClass 的关键区别是复用已有 provider,而不是在 TypeOrmModule 内再创建一个实例。编辑补充:这里的 ConfigService 必须满足 TypeORM 选项工厂契约,能够提供 createTypeOrmOptions();仅会 get() 的普通配置服务不能无条件替代这一接口。若使用命名数据源,name 仍写在这几个选项的同级。

自定义 DataSource 工厂

配合以上异步配置,还可以提供 dataSourceFactory,自行创建数据源。它接收已解析的 DataSourceOptions,返回解析为 DataSource 的 Promise。当前文档说明:如果返回的数据源尚未初始化,TypeOrmModule 会替它初始化,除非设置了 manualInitialization。

原文示例主动调用 initialize()。下面保留这一做法,继续把 synchronize 明确改成 false,并说明 options! 是 TypeScript 的非空断言,不是运行时校验:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: false,
  }),
  dataSourceFactory: async (options) => {
    const dataSource = await new DataSource(options!).initialize();
    return dataSource;
  },
});

DataSource 从 typeorm 导入。自定义工厂意味着应用更直接地参与初始化生命周期,应按当前库版本处理连接失败和清理;不能因函数声明为 async 就默认它不会失败。

继续阅读与版本说明

官方提供 Nest 仓库中的 TypeORM 示例应用。本文采用当前章节的 DataSource API 与 .js 导入后缀,后者应与项目的 TypeScript 输出和模块系统设置一致。不同示例片段各自说明一个装配选项,不应把重复定义的模块和服务全部放进同一个文件。

本文覆盖原章所有实质章节与例子,并对危险配置、事务失败传播、订阅器日志和工厂契约作了明确补充。没有连接数据库、运行示例或执行迁移;根实体、照片实体、控制器和配置服务等应用组成仍需读者按自己的项目实现。

来源、归属与许可

原文:TypeORM — NestJS,由 NestJS 文档维护者撰写与维护;文档仓库版权为 Kamil Myśliwiec,适用 MIT 许可证。本次于2026-10-05全文译写,保留版权与许可文本,原创绘制技术示意图。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE.txt

(The MIT License)

Copyright (c) 2017-present Kamil Myśliwiec <http://kamilmysliwiec.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容