NestJS的模块化架构配合TypeORM做数据库连接,核心就是把数据库配置、实体定义、仓储层分别放进对应的Module里,通过依赖注入把它们串起来。具体做法是:先用TypeORM模块的forRoot或forRootAsync注册数据库连接,再在每个业务模块里用forFeature注册对应的实体Repository,最后在Service层通过构造函数注入Repository来操作数据。这套流程跑通之后,你的项目就具备了高内聚、低耦合的特性,后期加功能模块只需要新建Module、注册实体、写Service,不用动任何数据库连接代码。

很多开发者刚接触NestJS时容易犯一个错误:把所有数据库配置和实体一股脑塞进app.module.ts。这样做短期能跑,但项目一大就会变成维护噩梦。正确的思路是按业务域拆分Module,每个Module只管自己那几张表的CRUD操作,数据库连接池和全局配置只在根模块里出现一次。下面我从搭建到实战,把每个环节都讲透。

一、项目初始化与依赖安装

用Nest CLI创建项目后,需要安装TypeORM和对应数据库驱动。以PostgreSQL为例,命令如下:

nest new my-project
cd my-project
npm install @nestjs/typeorm typeorm pg

如果用MySQL,把pg换成mysql2。如果用SQLite做开发测试,装sqlite3就行。安装完成后,项目里会多出@nestjs/typeorm和typeorm两个包,前者是NestJS对TypeORM的封装适配器,后者是TypeORM本体。

这里有个关键点:typeorm的版本选择要和@nestjs/typeorm兼容。目前NestJS 10.x对应的@nestjs/typeorm支持TypeORM 0.3.x,如果你还在用0.2.x的语法,比如createConnection,那就需要降级或者升级。建议直接用0.3.x的DataSource方式,这是官方推荐的新写法。

二、根模块中配置TypeORM数据库连接

在app.module.ts里,用TypeOrmModule.forRootAsync来注册数据库。为什么用forRootAsync而不是forRoot?因为实际项目中数据库密码、主机地址这些敏感信息不应该写死在代码里,应该从环境变量读取。forRootAsync支持useFactory模式,可以注入ConfigService。

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

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

这段代码有几个细节值得注意。第一,synchronize设为true只适合开发环境,生产环境必须关掉,否则TypeORM会自动同步表结构,可能导致数据丢失。第二,entities字段用了通配符匹配,autoLoadEntities设为true后TypeORM会自动扫描所有entity文件,你不需要手动在每个Module里再注册一遍实体。第三,如果你的实体文件分散在不同目录,用__dirname + '//*.entity{.ts,.js}'这种写法可以覆盖所有子目录。

还有一种情况是多数据库连接。比如你的项目同时要连MySQL和Redis(通过TypeORM也能做,但更常见的是用ioredis),可以在forRoot里加多个配置,或者用forRoot注册主库,再在子Module里用forRoot注册第二个库。不过多数场景一个主库就够了,多库会增加事务管理的复杂度。

三、定义实体类(Entity)

实体类就是数据库表的映射。每个业务模块都有自己的实体文件。比如用户模块,创建user.entity.ts:

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

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

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

  @Column()
  name: string;

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

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

这里用了几个常用装饰器:@Entity指定表名,@PrimaryGeneratedColumn定义主键(uuid类型比自增id更适合分布式系统),@Column加unique约束防止重复邮箱,@CreateDateColumn和@UpdateDateColumn自动维护时间戳。TypeORM 0.3.x的装饰器都从typeorm包直接导出,不需要从别的地方引入。

实体类的设计直接影响后续查询性能。比如经常按email查询用户,就要在@Column上加index: true。如果有多字段联合查询,可以用@Index装饰器定义复合索引。这些优化在开发阶段就要想好,后期加索引比改代码容易得多。

四、创建业务模块并注册TypeORM仓储

接下来创建users模块,在里面用TypeOrmModule.forFeature注册User实体对应的Repository。这样其他Module如果需要操作User表,就可以通过导入UsersModule来获取UserRepository。

// src/users/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],
  exports: [TypeOrmModule],
})
export class UsersModule {}

注意exports: [TypeOrmModule]这一行。如果其他模块需要直接注入UserRepository而不是通过UsersService,就必须把TypeOrmModule导出。但通常建议通过Service层封装业务逻辑,不要让Controller直接操作Repository,这样能保持职责分离。

forFeature的作用是告诉TypeORM:"这个Module要用到User这张表,请把UserRepository注入到这个Module的依赖容器里。"它不会创建新的数据库连接,只是在已有连接的基础上注册特定表的Repository。这就是NestJS模块化的精髓——连接共享、实体按需注册。

五、Service层实现具体数据库操作

Service是业务逻辑和数据访问的交汇点。通过构造函数注入Repository,然后封装增删改查方法:

// src/users/users.service.ts
import { Injectable, NotFoundException } 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 readonly userRepository: Repository<User>,
  ) {}

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

  async findOne(id: string): Promise<User> {
    const user = await this.userRepository.findOneBy({ id });
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  async create(data: Partial<User>): Promise<User> {
    const user = this.userRepository.create(data);
    return this.userRepository.save(user);
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const user = await this.findOne(id);
    this.userRepository.merge(user, data);
    return this.userRepository.save(user);
  }

  async remove(id: string): Promise<void> {
    const result = await this.userRepository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
  }
}

这段代码展示了最基础的CRUD。实际项目中你还会用到QueryBuilder做复杂查询、用分页、用关联查询(比如用户和订单的一对多关系)。TypeORM的Repository提供了find、findOne、findBy、create、save、remove、update等方法,覆盖了绝大多数场景。如果遇到特别复杂的SQL,可以用this.userRepository.createQueryBuilder('user')来手写查询,灵活性很高。

关于事务处理,NestJS配合TypeORM做事务非常方便。在Service方法上加@Transactional()装饰器(需要安装typeorm-transactional包),或者手动用dataSource.transaction包裹操作。多表写入时一定要用事务,否则部分成功部分失败会导致数据不一致。

六、多模块之间的实体关联与跨模块查询

当项目有多个模块时,实体之间往往有关系。比如Order模块的订单属于某个User。这时候需要在Order实体里定义关系:

// src/orders/entities/order.entity.ts
import { Entity, ManyToOne, JoinColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity('orders')
export class Order {
  // ...其他字段

  @ManyToOne(() => User, (user) => user.orders)
  @JoinColumn({ name: 'user_id' })
  user: User;
}

这里有个坑:如果User实体在users模块,Order实体在orders模块,那么OrdersModule的forFeature里需要同时导入User实体,否则TypeORM找不到关联的目标实体。解决办法是在OrdersModule里imports: [TypeOrmModule.forFeature([User])],或者直接在根模块的entities通配符里确保所有实体都被扫描到。

另一种更干净的做法是把共享实体放到一个common模块里,比如src/common/entities/,然后各业务模块从common导入。这样实体定义集中管理,不会出现循环依赖的问题。这是中大型项目的常见架构选择。

七、生产环境的数据库连接池优化

开发环境用默认连接池没问题,但生产环境必须调优。在TypeORM的DataSource配置里加上连接池参数:

useFactory: (configService: ConfigService) => ({
  type: 'postgres',
  // ...其他配置
  extra: {
    max: 20,           // 最大连接数
    min: 5,            // 最小连接数
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
  },
})

max值要根据你的服务器内存和PostgreSQL的max_connections来定。一般来说,Node.js应用每个实例20个连接,如果你部署了4个实例,就是80个连接,PostgreSQL默认max_connections是100,所以要留余量。如果用PgBouncer做连接池代理,那应用层的max可以设得更大,因为实际连接由PgBouncer管理。

还有一个容易忽略的点:TypeORM 0.3.x默认开启了日志,生产环境建议关掉或者设为error级别,否则大量SQL日志会拖慢性能。在配置里加logging: false或者logging: ['error']即可。

八、常见问题与排错思路

实际开发中最常遇到的问题有三个。第一,实体找不到报错"Entity metadata for User was not found"。这通常是因为实体文件没被扫描到,检查entities路径是否正确,或者autoLoadEntities是否开启。第二,模块导入报循环依赖。解决办法是用forwardRef或者把共享实体抽到common模块。第三,数据库连接超时。先检查环境变量是否正确加载,再检查防火墙和数据库是否允许远程连接,最后看连接池配置是否合理。

调试时可以开启TypeORM的query日志,在配置里加logging: true,这样每次执行SQL都会打印出来,方便定位慢查询和错误语句。配合NestJS的Logger模块,可以把数据库操作日志和业务日志分开记录,排查问题时效率翻倍。

总结一下,NestJS模块化加TypeORM数据库连接这套组合,核心逻辑就是"根模块管连接、子模块管实体、Service管操作"。把这个分层做好,项目无论怎么扩展都不会乱。实体设计要提前考虑索引和关联,连接池要根据实际负载调优,事务和错误处理要在Service层统一封装。掌握这些,你就能用这套技术栈搭建出可维护、可扩展的后端系统。