首页 / NestJS 入门教程 / TypeORM 集成

NestJS 入门教程

TypeORM 集成

本教程共 47 篇 · 第 26 篇 · 更新于 2026-08-09 · 约 12 分钟阅读

NestJSTypeORM数据库ORM实体Repository迁移

本节目标:在 NestJS 中接入 TypeORM,学会定义实体、使用仓库模式做增删改查、写事务和迁移。

做后端绕不开数据库。NestJS 对数据库的态度是”不绑定任何一家”,你想用什么都行。不过官方提供了几个开箱即用的集成包,其中 @nestjs/typeorm 是最成熟的一个。

TypeORM 是 TypeScript 生态里资历最老的 ORM 之一。它能用装饰器把类映射成数据库表,写起来跟写普通 TypeScript 类差不多。NestJS 跟它配合得很好,因为两边都是 TypeScript 优先。

安装依赖

先装包。以 MySQL 为例:

npm install @nestjs/typeorm typeorm mysql2

如果你用 PostgreSQL,把 mysql2 换成 pg;用 SQLite 就装 better-sqlite3。TypeORM 支持的数据库很多,流程基本一样,只是驱动包不同。

配置数据库连接

在根模块里用 TypeOrmModule.forRoot() 建立连接:

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'test',
      entities: [],       // 后面填实体类
      synchronize: true,  // 开发环境自动同步表结构
    }),
  ],
})
export class AppModule {}

这段代码做的事情很简单:告诉 TypeORM 数据库在哪、怎么连、哪些类是实体。

Warning

synchronize: true 在开发阶段很方便,但生产环境千万别开。它会根据实体定义自动改表结构,线上数据可能直接丢。生产环境请用迁移(migration)。

用环境变量配置

实际项目不会把数据库密码写死在代码里。配合 @nestjs/config 模块,从 .env 文件读取配置:

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USERNAME'),
        password: configService.get('DB_PASSWORD'),
        database: configService.get('DB_DATABASE'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: configService.get('NODE_ENV') !== 'production',
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

forRootAsync()forRoot() 的区别就是:前者支持异步,可以注入其他服务。这样数据库配置就能从环境变量、远程配置中心等地方动态获取。

Tip

entities 用 glob 路径 **/*.entity{.ts,.js} 可以自动扫描所有实体文件,不用一个个手动导入。这个写法在实际项目中用得最多。

NestJS 额外配置项

除了 TypeORM 原生的配置项,@nestjs/typeorm 还多了几个实用的选项:

选项说明默认值
retryAttempts连接失败重试次数10
retryDelay重试间隔(毫秒)3000
autoLoadEntities自动加载通过 forFeature 注册的实体false

autoLoadEntities 是个好东西,设成 true 之后,凡是在各模块里用 forFeature() 注册过的实体,都会自动加到连接配置里。不用在根模块手动维护实体列表了。

定义实体

实体就是数据库表的 TypeScript 映射。一个类对应一张表,类的属性对应表的列。

基本实体

// user.entity.ts
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

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

  @Column({ length: 100 })
  name: string;

  @Column({ unique: true })
  email: string;

  @Column({ select: false })
  password: string;

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

  @Column({ type: 'text', nullable: true })
  bio: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

@Entity('users') 里的 'users' 是表名。不传的话,TypeORM 会用类名的小写形式作为表名。建议显式指定,避免歧义。

@Columnselect: false 选项很实用。查数据时默认不会带上这个字段,适合密码这种敏感信息。需要的时候再手动 select。

常用列装饰器

装饰器作用
@PrimaryGeneratedColumn()自增主键
@PrimaryColumn()自定义主键(比如用 UUID)
@Column()普通列
@CreateDateColumn()创建时间,插入时自动填
@UpdateDateColumn()更新时间,每次更新自动刷新
@DeleteDateColumn()软删除标记,调用 softRemove 时自动填
@VersionColumn()乐观锁版本号,每次更新自动 +1
Note

@CreateDateColumn@UpdateDateColumn 是自动维护的,你不需要手动赋值。很多人会忘记这一点,在创建数据时手动传了个时间,结果被覆盖了。

列选项速查

@Column({
  type: 'varchar',     // 数据库列类型
  length: 100,         // varchar 长度
  nullable: false,     // 是否允许 NULL
  unique: true,        // 是否唯一
  default: 'active',   // 默认值
  select: false,       // 查询时是否默认包含
  comment: '用户邮箱',  // 数据库注释
})
email: string;

实体关系

数据库里表跟表之间的关系,在 TypeORM 里用装饰器声明。三种关系类型:一对一、一对多、多对多。

一对一

一个人对应一个档案:

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToOne(() => Profile, profile => profile.user)
  @JoinColumn()  // 外键在这张表上
  profile: Profile;
}

// profile.entity.ts
@Entity()
export class Profile {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  avatar: string;

  @OneToOne(() => User, user => user.profile)
  user: User;
}

@JoinColumn() 只能放在关系拥有外键的那一侧。就像”谁持有外键,谁负责声明”。

一对多

一个用户有多篇文章:

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @OneToMany(() => Post, post => post.author)
  posts: Post[];
}

// post.entity.ts
@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToOne(() => User, user => user.posts)
  author: User;
}

@OneToMany@ManyToOne 是成对出现的。外键在”多”的那一侧,也就是 Post 表里会有一个 authorId 字段。

多对多

用户和角色:一个用户可以有多个角色,一个角色也可以分配给多个用户。

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToMany(() => Role, role => role.users)
  @JoinTable()  // 中间表在这侧
  roles: Role[];
}

// role.entity.ts
@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => User, user => user.roles)
  users: User[];
}

@JoinTable() 告诉 TypeORM 在这张表这边建中间表。中间表的名字和字段名都可以自定义,不写的话 TypeORM 会自动生成。

仓库模式(Repository)

TypeORM 支持仓库模式——每个实体有自己的 Repository,封装了常用的数据库操作。NestJS 通过 @nestjs/typeorm 把它跟依赖注入无缝结合。

注册实体到模块

在功能模块里用 TypeOrmModule.forFeature() 注册实体:

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

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

forRoot() 管全局连接,forFeature() 管当前模块要用哪些实体。

注入 Repository

在 Service 里通过 @InjectRepository() 拿到 Repository 实例:

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@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 create(userData: Partial<User>): Promise<User> {
    const user = this.usersRepository.create(userData);
    return this.usersRepository.save(user);
  }

  async update(id: number, userData: Partial<User>): Promise<User | null> {
    await this.usersRepository.update(id, userData);
    return this.findOne(id);
  }

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

Repository 自带了很多方法:findfindOneBycreatesaveupdatedelete,基本上 CRUD 不用自己写 SQL。

Tip

create()save() 是两步。create() 只是把普通对象转成实体实例,不会操作数据库;save() 才会真正写入。很多人直接用 save(userData) 也能跑,但先 createsave 是更规范的写法。

跨模块使用 Repository

如果另一个模块也需要操作用户表,你需要把 TypeOrmModule 导出:

// users.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule],  // 导出,别的模块才能用
})
export class UsersModule {}

然后在其他模块导入 UsersModule 就行了。

查询构建器

Repository 的方法够应付简单场景。但实际开发中,复杂查询是家常便饭。TypeORM 提供了 QueryBuilder,用链式调用来构建 SQL。

基本查询

const users = await this.usersRepository
  .createQueryBuilder('user')
  .where('user.isActive = :isActive', { isActive: true })
  .orderBy('user.createdAt', 'DESC')
  .getMany();

createQueryBuilder('user') 里的 'user' 是别名,后面 where、orderBy 里都用这个别名来引用字段。

关联查询

const user = await this.usersRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .leftJoinAndSelect('post.comments', 'comment')
  .where('user.id = :id', { id: 1 })
  .getOne();

leftJoinAndSelect 会做 LEFT JOIN 并且把关联数据也查出来。如果只需要关联数据做条件过滤、不需要返回,用 leftJoin 就行,性能更好。

分页查询

async findWithPagination(page: number, limit: number) {
  const [data, total] = await this.usersRepository
    .createQueryBuilder('user')
    .skip((page - 1) * limit)
    .take(limit)
    .getManyAndCount();

  return {
    data,
    total,
    page,
    limit,
    totalPages: Math.ceil(total / limit),
  };
}

skip 跳过多少条,take 取多少条,getManyAndCount 同时返回数据和总数。分页接口标配。

复杂条件

import { Brackets } from 'typeorm';

const users = await this.usersRepository
  .createQueryBuilder('user')
  .select(['user.id', 'user.name', 'user.email'])
  .where('user.isActive = :isActive', { isActive: true })
  .andWhere(
    new Brackets(qb => {
      qb.where('user.name LIKE :name', { name: '%John%' })
        .orWhere('user.email LIKE :email', { email: '%@example.com%' });
    }),
  )
  .orderBy('user.createdAt', 'DESC')
  .getMany();

Brackets 用来分组条件。上面这段生成的 SQL 是 WHERE isActive = true AND (name LIKE '%John%' OR email LIKE '%@example.com%')。不用 Brackets 的话,OR 条件会跟外层条件混在一起,逻辑就乱了。

Note

QueryBuilder 里的参数用 :参数名 的格式,通过第二个参数对象传值。不要直接拼字符串,那样会有 SQL 注入风险。

事务处理

数据库事务保证一组操作要么全部成功,要么全部回滚。比如转账:A 扣钱和 B 加钱必须同时成功,不能只做一半。

使用 QueryRunner(推荐)

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './entities/user.entity';

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

  async createWithProfile(userData: any, profileData: any) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const user = queryRunner.manager.create(User, userData);
      await queryRunner.manager.save(user);

      // 其他操作...

      await queryRunner.commitTransaction();
      return user;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();  // 一定要释放
    }
  }
}

DataSourceTypeOrmModule.forRoot() 之后就可以全局注入,不需要额外 import 模块。

这个模式很固定:创建 QueryRunner -> 连接 -> 开启事务 -> 操作 -> 提交/回滚 -> 释放。finally 里的 release() 不能忘,不然连接池会被耗尽。

Warning

很多人忘记在 finally 里释放 QueryRunner,导致数据库连接池被占满,服务直接挂掉。这不是理论问题,是真实踩坑。

使用 transaction 方法(简洁写法)

如果不需要精细控制,可以用回调式的写法:

async createWithProfile(userData: any, profileData: any) {
  await this.dataSource.transaction(async manager => {
    const user = manager.create(User, userData);
    await manager.save(user);

    const profile = manager.create(Profile, profileData);
    await manager.save(profile);
  });
}

回调函数里的 manager 就是事务内的 EntityManager。如果回调抛出异常,事务自动回滚;正常结束就自动提交。代码量更少,也不用操心释放。

数据库迁移

synchronize: true 在开发阶段自动同步表结构,但生产环境不能依赖它。迁移(migration)才是正确做法——每次表结构变更都生成一个迁移文件,像 git commit 一样记录变更历史。

创建迁移

npx typeorm migration:create migrations/CreateUserTable

这会生成一个空的迁移文件,你需要手动填写 updown 方法:

// migrations/1700000000000-CreateUserTable.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class CreateUserTable1700000000000 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'users',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'name',
            type: 'varchar',
            length: '100',
          },
          {
            name: 'email',
            type: 'varchar',
            length: '100',
            isUnique: true,
          },
          {
            name: 'createdAt',
            type: 'datetime',
            default: 'CURRENT_TIMESTAMP',
          },
        ],
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('users');
  }
}

up 是正向操作(建表),down 是逆向操作(删表)。这样任何时候都能回滚。

运行迁移

package.json 里配置脚本:

{
  "scripts": {
    "typeorm": "typeorm-ts-node-commonjs",
    "migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
    "migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts",
    "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts"
  }
}

你需要单独创建一个 data-source.ts 文件给 TypeORM CLI 用:

// data-source.ts
import { DataSource } from 'typeorm';

export default new DataSource({
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  username: 'root',
  password: 'password',
  database: 'test',
  entities: ['src/**/*.entity.ts'],
  migrations: ['migrations/*.ts'],
});
Tip

migration:generate 可以自动对比数据库现状和实体定义,生成差异迁移文件。比手写靠谱,不容易漏字段。

迁移的注意事项

迁移文件不归 NestJS 管,是 TypeORM CLI 维护的。所以迁移里不能用依赖注入,不能引用 NestJS 的模块系统。把它当成独立的数据库脚本就行。

事件订阅者(Subscriber)

TypeORM 支持监听实体事件,比如插入前、更新后。这在需要自动处理某些逻辑时很有用,比如密码加密。

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

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

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log('准备插入用户:', event.entity);
    // 比如在这里做密码加密
  }
}

UserSubscriber 加到模块的 providers 数组就能生效。

Note

事件订阅者不能是请求作用域的(request-scoped),它跟 DataSource 的生命周期绑定。

测试时的 Mock

单元测试不想连真实数据库。NestJS 提供了 getRepositoryToken() 来生成 Repository 的注入令牌,方便用 mock 替换:

import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

const mockRepository = {
  find: jest.fn().mockResolvedValue([]),
  findOneBy: jest.fn().mockResolvedValue(null),
  create: jest.fn().mockReturnValue({}),
  save: jest.fn().mockResolvedValue({}),
  delete: jest.fn().mockResolvedValue({}),
};

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

这样 UsersService 注入的 Repository 就是 mock 对象,不会真正操作数据库。

多数据库连接

有些项目需要连多个数据库,比如用户数据在一个库,订单数据在另一个库。TypeORM 支持给连接命名:

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'user_db_host',
      port: 5432,
      username: 'user',
      password: 'password',
      database: 'user_db',
      entities: [User],
      synchronize: true,
    }),
    TypeOrmModule.forRoot({
      type: 'postgres',
      name: 'orderConnection',  // 给连接起名
      host: 'order_db_host',
      port: 5432,
      username: 'user',
      password: 'password',
      database: 'order_db',
      entities: [Order],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

默认连接的 name 是 'default'。有名字的连接在 forFeature@InjectRepository 里也要指定:

TypeOrmModule.forFeature([Order], 'orderConnection')
constructor(
  @InjectRepository(Order, 'orderConnection')
  private orderRepository: Repository<Order>,
) {}

小结

TypeORM 跟 NestJS 的配合已经很成熟了。核心流程就是:forRoot 建连接、定义实体类、forFeature 注册到模块、@InjectRepository 注入使用。

几个容易踩的坑再强调一下:

  • synchronize: true 只在开发环境用
  • QueryRunner 用完一定要 release()
  • create() 只是创建实例,save() 才写数据库
  • 迁移文件独立于 NestJS,不能用依赖注入
  • 多数据库连接时,name 要在 forRootforFeature 两侧都指定

掌握了这些,TypeORM 的日常使用就够用了。下一章咱们看看另一个热门选择——Prisma。