NestJS 多租户数据库连接如何优雅透传?nestjs-cls 实战多租户上下文存储
2026/8/24 8:43:02 网站建设 项目流程

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,继续局部存储)可以理解为"请求级的线程局部存储":

  1. 请求到达时,通过cls.run()建立一个上下文;
  2. 在上下文内用cls.set()/cls.get()读写数据;
  3. 同一请求回调链中的任何代码(包括 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.mddocs/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 条最佳实践清单 ✅

  1. 统一入口写入:租户标识只在中间件 / 拦截器的setup钩子中写入一次,禁止散落在业务代码里;
  2. 用类型安全 Store:为ClsStore声明tenantId等字段,让 IDE 和编译器帮你守住数据隔离的底线;
  3. 连接交给 Proxy Provider:保持 Provider 单例,按需动态解析,兼顾性能与隔离;
  4. 开启 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询