NestJS CQRS命令总线多租户场景依赖注入异常求助
问题分析
CQRS的Command Handler默认是单例作用域,而你的StateRepository依赖了请求作用域的DbConnectionProvider。NestJS不允许单例对象注入请求作用域依赖,会导致依赖解析失败,最终出现_stateRepository为undefined的情况。同时,Command Bus执行命令时脱离了原请求上下文,无法直接获取请求对象中的数据库连接。
解决方案
方案一:将Command Handler改为请求作用域
直接修改Command Handler的作用域为请求作用域,使其能正常注入请求作用域的依赖:
@CommandHandler(SomeCommand) @Injectable({ scope: Scope.REQUEST }) // 添加请求作用域声明 export class SomeCommandHandler implements ICommandHandler<SomeCommand> { constructor( private _logger: WinstonLogger, private _stateRepository: StateRepository, ) {} async execute(command: SomeCommand): Promise<boolean> { this._logger.info(`At command handler`); const state = await this._stateRepository.getById(command.id); // 业务逻辑处理 return true; } }
注意:请求作用域的实例会在每个请求时重新创建,可能带来一定性能开销,需结合业务场景评估是否接受。
方案二:在命令中携带tenantId,主动获取数据库连接
让命令自身携带执行所需的tenantId,Command Handler不再依赖请求上下文,直接通过TenancyService获取连接:
- 修改命令定义,新增
tenantId字段:
export class SomeCommand { constructor( public readonly tenantId: string, // 携带租户标识 public readonly clientId: string, public readonly savedClientGoal: any, public readonly user: any, ) {} }
- 调整Command Handler逻辑:
@CommandHandler(SomeCommand) export class SomeCommandHandler implements ICommandHandler<SomeCommand> { constructor( private _logger: WinstonLogger, private _tenancyService: TenancyService, ) {} async execute(command: SomeCommand): Promise<boolean> { this._logger.info(`At command handler`); // 根据tenantId直接获取数据库连接 const dbConnection = await this._tenancyService.getDbConnection(command.tenantId); const stateRepository = dbConnection.getRepository(States); // 数据库操作 const state = await stateRepository.findOneBy({ id: command.id }); // 手动销毁连接(或复用连接池,根据TenancyService逻辑调整) await dbConnection.destroy(); return true; } }
优势:符合CQRS设计原则(命令包含所有执行所需信息),无需依赖请求上下文。
方案三:使用AsyncLocalStorage传递请求上下文(推荐)
利用NestJS支持的AsyncLocalStorage在请求开始时保存上下文(如数据库连接),让Command Handler在上下文范围内获取连接,同时保持单例特性:
- 创建上下文存储服务:
@Injectable() export class RequestContextService { private readonly als = new AsyncLocalStorage<Map<string, any>>(); runWithContext(context: Map<string, any>, callback: () => void): void { this.als.run(context, callback); } get<T>(key: string): T | undefined { const context = this.als.getStore(); return context?.get(key) as T; } }
- 修改
DatabaseMiddleware,将连接存入上下文:
@Injectable({ scope: Scope.REQUEST }) export class DatabaseMiddleware implements NestMiddleware { constructor( private readonly tenancyService: TenancyService, private readonly logger: WinstonLogger, private readonly requestContextService: RequestContextService, ) {} async use(req: Request, res: Response, next: any): Promise<void> { const dbConnection = await this.executeDbMiddleware(req); // 存储连接到请求上下文 const context = new Map<string, any>(); context.set('dbConnection', dbConnection); // 用上下文包裹后续请求流程 this.requestContextService.runWithContext(context, () => { next(); }); // 请求结束后销毁连接 res.on('finish', async () => { await dbConnection.destroy(); }); } }
- 修改
DbConnectionProvider,从上下文获取连接:
@Injectable() // 改为单例作用域 export class DbConnectionProvider { constructor(private readonly requestContextService: RequestContextService) {} getConnection(): DataSource { const dbConnection = this.requestContextService.get<DataSource>('dbConnection'); if (dbConnection) { return dbConnection; } // fallback到默认连接 return getConnectionManager().get(); } }
- Command Handler保持单例,正常注入依赖:
@CommandHandler(SomeCommand) export class SomeCommandHandler implements ICommandHandler<SomeCommand> { constructor( private _logger: WinstonLogger, private _stateRepository: StateRepository, ) {} async execute(command: SomeCommand): Promise<boolean> { this._logger.info(`At command handler`); const state = await this._stateRepository.getById(command.id); // 业务逻辑处理 return true; } }
优势:既保留了单例的性能优势,又能无缝传递请求上下文,是最优雅的解决方案。
内容的提问来源于stack exchange,提问作者Abhishek
相关产品推荐
相关产品推荐

