NestJS中需用@Injectable({ scope: Scope.REQUEST })的MongooseConfigService动态生成含租户名的URI,确保连接隔离;GridFSBucket须按租户ID动态创建并规范命名;数据库名须正则校验且密码URL编码;多库仅适用于数据量悬殊或合规强隔离场景。

如何用 MongooseModule.forRootAsync 动态切换数据库
不能靠 use 命令或手动调用 db.getSiblingDB() —— 那是 shell 里的操作,Node.js 驱动不认。NestJS 必须在模块初始化阶段就确定每个请求该连哪个库,核心是让 MongooseModule.forRootAsync 每次返回不同的 uri。
关键点在于:必须把 @Injectable({ scope: Scope.REQUEST }) 加到配置服务上,否则所有请求共享同一个连接实例,租户数据会串库。
-
MongooseConfigService构造函数里通过@Inject(REQUEST)拿到当前请求上下文,从中提取租户标识(比如req.headers['x-tenant-id']或解析 JWT) -
createMongooseOptions()返回的uri必须包含完整数据库名,例如mongodb://localhost:27017/tenant_acme,不能只写 host 和 port - 不要在 uri 后面拼
?authSource=admin就完事——如果租户库启用了独立认证,得确保连接用户有权限访问那个具体库,而不是只配authSource
为什么不能复用全局 GridFSBucket 实例
GridFS 的 bucketName 是创建 GridFSBucket 实例时就绑定死的,不是运行时可变参数。如果你在模块顶层 new GridFSBucket(db, { bucketName: 'fs' }),那所有租户都往同一个 fs.files 和 fs.chunks 里写,彻底失去隔离。
正确做法是在 Controller 或 Service 中按需构造:
const bucket = new mongodb.GridFSBucket(db, {
bucketName: `tenant_${tenantId}_files`
});
-
tenantId必须做规范化:过滤掉非字母数字下划线字符,防止非法集合名(MongoDB 不允许.、$等出现在集合名中) - 别用 Map 缓存
bucket实例——除非你明确控制生命周期,否则容易内存泄漏;短生命周期请求里直接 new 更安全 - 如果租户数超千级,注意 MongoDB 对 namespace 数量的限制(尤其分片集群),此时应优先考虑单库 +
tenant_id字段隔离
租户数据库名生成与权限校验的硬性约束
数据库名不能来自用户直输,也不能未经校验就拼进 URI。MongoDB 对数据库名有严格限制:^[a-zA-Z0-9_\-]+$,且不能以数字开头。一旦传入 tenant-123. 这类非法名,Mongoose.connect() 会静默失败或抛出难以定位的 InvalidNamespace 错误。
- 从请求提取租户标识后,必须用正则清洗:
tenantId.replace(/[^a-z0-9_\-]/g, '_').replace(/^\d/, '_') - 连接字符串中的密码若含特殊字符(如
@、/),必须 URL 编码,否则mongodb://user:pass@host/db解析会错乱 - 生产环境必须为每个租户库创建专用数据库用户,并只授予其对应库的
readWrite权限——绝不能给readWriteAnyDatabase角色
单集合租户字段隔离 vs 多数据库的取舍边界
多数据库方案看似彻底,但运维成本陡增:备份脚本要动态发现所有 tenant_* 库,mongodump 参数得实时生成;租户删除时,得主动执行 db.dropDatabase(),漏掉就留僵尸库;分片集群下 namespace 元数据膨胀会拖慢 config server。
真正该选多库的场景极少:
- 租户数据量级差异极大(一个租户占集群 80% 存储)
- 合规要求物理磁盘隔离(如金融、医疗行业审计条款)
- 需要对单个租户做独立备份/恢复/迁移
其余情况,用单库 + tenant_id 字段 + 复合索引 + JSON Schema 校验,更轻量、更可控。这时候重点不是“怎么切库”,而是“怎么确保每条查询都带 tenant_id”。


















