NestJS 多租户数据库连接如何优雅透传?nestjs-cls 实战多租户上下文存储
【免费下载链接】nestjs-clsA continuation-local storage (async context) module compatible with NestJS's dependency injection.项目地址: https://gitcode.com/gh_mirrors/ne/nestjs-cls
在 NestJS 多租户应用中,让每个请求"记住"自己属于哪个租户、使用哪个数据库连接,往往是架构里最头疼的一环。这篇文章带你使用nestjs-cls(一个兼容 NestJS 依赖注入的异步上下文存储模块)实现多租户数据库连接的优雅透传:请求进来时存入租户标识,业务层任何位置都能无感获取当前租户的连接,不再需要在每个方法里层层手动传参。
多租户数据库连接的三大痛点 💡
假设你正在开发 SaaS 产品,每个租户的数据都隔离在不同的数据库(或不同 Schema)中。传统的做法会遇到这些问题:
- 手动传参繁琐:Controller → Service → Repository,每一层都要显式传递
tenantId或连接对象,代码被"污染"; - REQUEST 作用域的陷阱:把连接声明为请求作用域的 Provider,会导致整个 DI 子树随请求重建,性能差且无法在 WebSocket、定时任务等场景使用;
- 事务难以共享:跨服务的事务引用如何传递?显式传递会破坏封装,隐式全局状态又容易串数据。
这正是 README.md 中列出的核心场景之一:"Making the dynamic Tenant database connection available everywhere in multi-tenant apps"(让动态租户数据库连接在多租户应用中随处可用)。
先搞懂原理:什么是 CLS 异步上下文
CLS(Continuation-Local Storage,继续局部存储)可以理解为"请求级的线程局部存储":
- 请求到达时,通过
cls.run()建立一个上下文; - 在上下文内用
cls.set()/cls.get()读写数据; - 同一请求回调链中的任何代码(包括 Promise、异步函数深处)都能访问到这份存储。
它底层基于 Node.js 官方的AsyncLocalStorage,天然支持异步传播,且不改变任何 Provider 的作用域——这正是它比 REQUEST 作用域优雅的关键。原理详解可参考文档docs/docs/01_introduction/03_how-it-works.md。
一键安装与模块注册:3 步接入 ⚡
安装方式与任意 NPM 包相同(如需离线克隆源码,仓库地址为 https://link.gitcode.com/i/f9884d2c1b5a8376dcce92c700034459):
npm install nestjs-cls然后在应用根模块注册(挂载一个内置中间件即可全局生效):
ClsModule.forRoot({ middleware: { mount: true }, });完整安装与注册说明见docs/docs/01_introduction/01_installation.md和docs/docs/01_introduction/02_quick-start.md。
核心技巧:用 setup 钩子一行存入租户上下文 ✍️
这是多租户透传的"第一步":在请求进入业务层之前,把租户 ID 存进 CLS。setup钩子会在上下文建立后自动执行,并拿到Request对象:
ClsModule.forRoot({ middleware: { mount: true, setup: (cls, req) => { cls.set('TENANT_ID', req.params['tenantId']); }, }, });从此,任意深度的 Service只需注入ClsService就能拿到租户 ID,彻底告别层层传参。官方推荐的完整写法见docs/docs/03_features-and-use-cases/02_additional-cls-setup.md。
💡 小技巧:配合类型安全的 CLS Store,
this.cls.get('TENANT_ID')会获得完整类型推断,拼错 key 在编译期就会报错。详见docs/docs/03_features-and-use-cases/05_type-safety-and-type-inference.md。
进阶利器:Proxy Provider 动态解析租户连接 🚀
存了TENANT_ID之后,怎么让每个租户的数据库连接"自动"注入到 Service?答案:Proxy Provider(代理 Provider)——这是 nestjs-cls 从 Spring 框架"请求 Bean"中汲取灵感的杀手级特性。
它的巧妙之处在于:
- 注入的其实是一个单例 Proxy 对象,不会污染宿主 Provider 的作用域;
- 每次请求时,工厂函数根据当前 CLS 上下文(比如
TENANT_ID)动态创建真正的连接实例,存入上下文; - 之后 Service 里像使用普通对象一样访问它,底层自动路由到对应租户的连接。
文档中正好给出了一个"根据请求参数动态解析租户数据库连接"的示例(docs/docs/03_features-and-use-cases/06_proxy-providers.md):工厂函数从请求中取出tenantId,调用dbService.getTenantConnection(tenantId)返回连接,之后任何注入该 token 的 Service 拿到的就是当前租户专属的连接。
它还支持:
- 延迟解析:通过
resolveProxyProviders: false+ 手动cls.proxy.resolve(),在上下文信息更完整时再解析; - 严格模式(
strict: true):上下文未就绪时访问会直接抛错,避免"静默返回空对象"的隐蔽 Bug。
事务也想透传?交给 Transactional 插件 🧩
多租户场景中,跨 Service 的数据库事务同样需要"透传"。官方@nestjs-cls/transactional插件把事务引用也存进 CLS:
- 用
TransactionHost.withTransaction()或@Transactional()装饰器开启事务; - 后续任何 Service 通过
txHost.tx获取的就是同一个事务,无需显式传参; - 官方适配器覆盖Prisma、Knex、Kysely、TypeORM、Drizzle ORM、Pg-promise、MongoDB、Mongoose等主流库,且无需 monkey-patch。
源码位于packages/transactional/,使用文档见docs/docs/06_plugins/01_available-plugins/01-transactional/index.md,各适配器文档同在docs/docs/06_plugins/01_available-plugins/01-transactional/目录下。
多租户落地的 4 条最佳实践清单 ✅
- 统一入口写入:租户标识只在中间件 / 拦截器的
setup钩子中写入一次,禁止散落在业务代码里; - 用类型安全 Store:为
ClsStore声明tenantId等字段,让 IDE 和编译器帮你守住数据隔离的底线; - 连接交给 Proxy Provider:保持 Provider 单例,按需动态解析,兼顾性能与隔离;
- 开启 strict 模式:宁可快速失败,也不要让未解析的代理静默返回空对象。
总结
nestjs-cls 的多租户上下文存储方案,把"租户 ID → 数据库连接 → 事务"整条链路装进了一个请求级上下文:中间件写入、Proxy Provider 动态解析、Transactional 插件透传事务,三层组合拳让多租户数据库连接在整个 NestJS 应用中优雅透传——业务代码零侵入,依赖注入习惯零改变。
📚 延伸阅读(项目内文档路径):
- 模块选项参考:
docs/docs/04_api/02_module-options.md - 非 Web 请求场景(定时任务、队列):
docs/docs/03_features-and-use-cases/04_usage-outside-of-web-request.md - 核心服务实现:
packages/core/src/lib/cls.service.ts
【免费下载链接】nestjs-clsA continuation-local storage (async context) module compatible with NestJS's dependency injection.项目地址: https://gitcode.com/gh_mirrors/ne/nestjs-cls
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考