从三套 ORM 集成到动态模块
前三节完成了 Prisma、TypeORM、Mongoose 的集成,但目前的配置方式都是静态的——启动时读取固定的 .env 配置,只能连接一个数据库。通用模板项目需要支持多租户场景:不同租户连接不同数据库,这就要求模块能动态接收配置。
请求进入 → 解析租户信息 → 匹配数据库配置 → 获取对应 ORM 实例 → 操作数据库
text
实现这种能力的关键是 NestJS 的**动态模块(Dynamic Module)**机制。
静态模块的局限
常规 NestJS 模块是静态的:
@Module({
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
typescript
消费者(import 方)无法影响提供者(被 import 方)的配置和行为。如果 UserModule 需要在不同场景下表现出不同的行为(比如连接不同的数据库),静态模块就无能为力了。
动态模块核心概念
动态模块的官方定义:
动态模块提供了一种 API,将模块导入到另一个模块时,可以在定义模块属性时影响其属性或行为。
简单说:动态模块允许我们在注册模块的过程中,通过参数定制其配置和行为。
DynamicModule 接口
// @nestjs/common
interface DynamicModule extends ModuleMetadata {
module: Type<any>;
providers?: Provider[];
exports?: (Type<any> | DynamicModule | string)[];
// ... 与 @Module() 相同的结构
}
typescript
动态模块返回的结构与静态模块完全一致,只是通过静态方法(而非装饰器)动态生成。
register / forRoot / forFeature 命名约定
NestJS 社区约定了三种命名模式:
| 方法名 | 用途 | 示例 |
|---|---|---|
forRoot | 全局配置,只调用一次 | TypeOrmModule.forRootAsync(...) |
forFeature | 局部注册,按需调用 | TypeOrmModule.forFeature([User]) |
register | 一次性注册,每次调用独立配置 | ConfigModule.register({ folder: './config' }) |
每种都有异步变体(forRootAsync、registerAsync),支持通过 DI 注入依赖。
动态模块工作原理
以 ConfigModule.register() 为例,剖析动态模块的完整工作流:
1. 定义静态方法
// config/config.module.ts
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options: { folder: string }): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
typescript
2. 消费者调用
// app.module.ts
@Module({
imports: [
ConfigModule.register({ folder: './config' }),
],
})
export class AppModule {}
typescript
3. Service 中通过 DI 接收参数
// config/config.service.ts
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class ConfigService {
constructor(
@Inject('CONFIG_OPTIONS')
private readonly options: { folder: string },
) {}
get(key: string): string {
// 根据 this.options.folder 读取配置文件...
}
}
typescript
执行流程
1. AppModule 初始化 → 遇到 ConfigModule.register({ folder: './config' })
2. 调用 register 静态方法 → 返回 DynamicModule
3. DynamicModule 中注册了 'CONFIG_OPTIONS' Provider(值为 { folder: './config' })
4. DI 容器初始化 ConfigService → 通过 @Inject('CONFIG_OPTIONS') 拿到 options
5. ConfigService 使用 options.folder 读取配置文件
text
核心机制:通过 provide + useValue/useFactory 将参数注册为 DI Provider,Service 通过 @Inject() 获取。
forRootAsync 的异步配置
当配置需要依赖其他服务(如 ConfigService 读取环境变量)时,使用异步变体:
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: configService.get('DB_TYPE'),
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
synchronize: false,
autoLoadEntities: true,
}),
});
typescript
| 参数 | 说明 |
|---|---|
inject | 声明需要注入的依赖(DI 容器解析后传入 useFactory) |
useFactory | 工厂函数,接收注入的依赖,返回模块配置 |
imports | 可选,导入工厂函数需要的额外模块 |
这就是之前 TypeORM 和 Mongoose 集成中使用 forRootAsync 的原理。
Prisma 多租户改造方向
当前 PrismaService 只支持单个数据库连接。多租户场景需要改造为:
// 目标架构(下节实现)
PrismaModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
// 根据传入参数动态拼接 DATABASE_URL
// 不同租户 → 不同的 URL → 不同的 PrismaClient 实例
// 实例存入 Map<tenantId, PrismaClient>
},
})
typescript
Prisma 官方支持程序化覆盖数据源 URL,通过构造 PrismaClient 时传入 datasources.db.url 参数实现:
const client = new PrismaClient({
datasources: {
db: {
url: `mysql://${user}:${password}@${host}:${port}/${database}`,
},
},
});
typescript
AOP 思想在动态模块中的体现
动态模块是**面向切面编程(AOP)**思想在 NestJS 中的具体落地。消费者不需要关心模块内部的实现细节,只需要通过参数接口定制行为——这正是分层化编程的核心:
| 层次 | 职责 | 示例 |
|---|---|---|
| 模块提供者 | 定义可定制的行为和 Provider | ConfigModule.register() |
| 模块消费者 | 通过参数定制模块行为 | ConfigModule.register({ folder: './config' }) |
| DI 容器 | 自动解析依赖并注入 | @Inject('CONFIG_OPTIONS') |
前端开发者可能对这种模式不太熟悉,但它和 React 的高阶组件(HOC)或 Vue 的插件注册(app.use(plugin, options))在思想上是相通的——通过参数注入来定制组件/模块的行为。
下一步
下一节将基于动态模块原理,实现 Prisma 的 forRootAsync 多租户版本,支持运行时动态切换数据库连接。
↑