TypeDI按请求注入数据库连接:线程安全与最优实现咨询
关于TypeDI+Routing-Controllers多数据库请求注入的问题解答
并行请求下现有方案的问题
两种现有实现方式都会导致Prisma实例混用,原因如下:
- 方式一(调用
.initPrisma传入实例):TypeDI默认服务为单例模式,多个并行请求会同时修改同一个服务实例的Prisma属性,后发起的请求会覆盖先发起请求的实例,导致数据查询串库。 - 方式二(
Container.set全局设置实例):Container.set操作的是全局容器,所有请求共享同一个容器实例,并行请求会互相覆盖全局的PrismaService,必然导致实例混用。
更优的依赖注入方案:请求作用域+Vhost中间件
利用TypeDI的**请求作用域(Request Scope)**结合Routing-Controllers的请求上下文,为每个请求注入对应Vhost的Prisma实例,完全避免并行请求冲突。具体实现步骤如下:
1. 配置容器与路由控制器
确保Routing-Controllers使用TypeDI作为依赖容器:
import { useContainer, createExpressServer } from 'routing-controllers'; import { Container } from 'typedi'; useContainer(Container); const app = createExpressServer({ controllers: [__dirname + '/controllers/*.ts'], middlewares: [__dirname + '/middlewares/*.ts'], }); app.listen(3000);
2. 实现Prisma实例工厂(复用实例)
创建工厂类负责根据Vhost创建/复用PrismaClient实例,避免重复初始化:
import { Service } from 'typedi'; import { PrismaClient } from '@prisma/client'; @Service() export class PrismaFactory { // 缓存已创建的Prisma实例,按Vhost区分 private instanceCache = new Map<string, PrismaClient>(); getInstance(vhost: string): PrismaClient { if (!this.instanceCache.has(vhost)) { const dbUrl = this.getDbUrlByVhost(vhost); const prisma = new PrismaClient({ datasources: { db: { url: dbUrl }, }, }); this.instanceCache.set(vhost, prisma); } return this.instanceCache.get(vhost)!; } // 根据Vhost匹配对应数据库连接串(可从配置文件/环境变量读取) private getDbUrlByVhost(vhost: string): string { const dbConfig = { 'shopA.example.com': 'postgresql://user:pass@db-shopA:5432/shopA', 'shopB.example.com': 'postgresql://user:pass@db-shopB:5432/shopB', 'default.example.com': 'postgresql://user:pass@db-default:5432/default', }; return dbConfig[vhost] || dbConfig['default.example.com']; } }
3. 编写Vhost中间件注入请求作用域实例
在请求进入控制器前,解析Vhost并将对应Prisma实例注入当前请求的子容器:
import { Middleware, ExpressMiddlewareInterface } from 'routing-controllers'; import { Container } from 'typedi'; import { PrismaFactory } from '../services/PrismaFactory'; @Middleware({ type: 'before' }) export class VhostDbMiddleware implements ExpressMiddlewareInterface { use(req: any, res: any, next: () => void): void { // 解析请求的Vhost(去除端口号) const vhost = req.headers.host?.split(':')[0] || 'default.example.com'; // 从全局容器获取工厂实例,创建对应Vhost的PrismaClient const prismaFactory = Container.get(PrismaFactory); const prisma = prismaFactory.getInstance(vhost); // 将实例注入当前请求的子容器,标记为请求作用域 Container.of(req).set('PrismaClient', prisma); next(); } }
4. 服务与控制器中注入使用
在业务服务中直接注入请求作用域的Prisma实例:
import { Service } from 'typedi'; import { Inject } from 'typedi'; import { PrismaClient } from '@prisma/client'; @Service() export class OrderService { constructor(@Inject('PrismaClient') private prisma: PrismaClient) {} async getOrderById(orderId: number) { return this.prisma.order.findUnique({ where: { id: orderId } }); } }
控制器中注入服务即可正常使用:
import { Controller, Get, Param } from 'routing-controllers'; import { Inject } from 'typedi'; import { OrderService } from '../services/OrderService'; @Controller('/orders') export class OrderController { constructor(@Inject() private orderService: OrderService) {} @Get('/:id') async getOrder(@Param('id') id: number) { return this.orderService.getOrderById(id); } }
方案优势
- 无实例混用:每个请求拥有独立的子容器,注入的Prisma实例仅属于当前请求,并行请求完全隔离。
- 性能优化:通过工厂类缓存Prisma实例,避免每个请求重复初始化数据库连接。
- 代码解耦:业务服务无需关心数据库选择逻辑,仅依赖注入的Prisma实例即可。
内容的提问来源于stack exchange,提问作者Leandro Costa
相关产品推荐
相关产品推荐

