如何在NestJS/TypeORM中实现基于列的多租户且无需传递companyId?
基于NestJS + TypeORM的无样板多租户实现(共享库/共享Schema)
针对你提出的问题,我们可以通过AsyncLocalStorage(ALS)存储请求上下文 + TypeORM订阅器/自定义仓库的组合方案,彻底消除手动传递companyId的样板代码,同时避免请求作用域带来的性能损耗。
核心思路
- 从请求中提取
companyId,存入Node.js原生的AsyncLocalStorage,让整个请求链路都能安全获取该值 - 在TypeORM层面自动完成两件事:
- 新增/更新实体时,自动注入当前请求的
companyId - 查询操作时,自动拼接
companyId过滤条件,确保租户数据隔离
- 新增/更新实体时,自动注入当前请求的
步骤1:实现请求上下文管理(基于AsyncLocalStorage)
1.1 创建上下文服务
// src/common/context/tenant.context.ts import { AsyncLocalStorage } from 'async_hooks'; import { Injectable, UnauthorizedException } from '@nestjs/common'; export interface TenantContext { companyId: string; } export const tenantStorage = new AsyncLocalStorage<TenantContext>(); @Injectable() export class TenantContextService { setCompanyId(companyId: string) { tenantStorage.enterWith({ companyId }); } getCompanyId(): string { const context = tenantStorage.getStore(); if (!context?.companyId) { throw new UnauthorizedException('租户ID未找到'); } return context.companyId; } }
1.2 编写拦截器提取并注入companyId
// src/common/interceptors/tenant.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common'; import { Observable } from 'rxjs'; import { TenantContextService } from '../context/tenant.context'; @Injectable() export class TenantInterceptor implements NestInterceptor { constructor(private readonly tenantContextService: TenantContextService) {} async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> { const request = context.switchToHttp().getRequest(); // 根据实际场景获取companyId:JWT令牌解析/查询参数 const companyId = request.user?.companyId || request.query.companyId; if (!companyId) { throw new UnauthorizedException('缺少租户标识'); } this.tenantContextService.setCompanyId(companyId); return next.handle(); } }
1.3 全局注册拦截器
在main.ts中全局启用拦截器:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { TenantInterceptor } from './common/interceptors/tenant.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(app.get(TenantInterceptor)); await app.listen(3000); } bootstrap();
步骤2:TypeORM层面自动处理租户隔离
2.1 定义租户实体基类
让所有需要租户隔离的实体继承这个基类:
// src/common/entities/tenant.entity.ts import { Column, PrimaryGeneratedColumn } from 'typeorm'; export abstract class TenantEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'uuid', nullable: false }) companyId: string; }
示例Cat实体改造:
// src/cats/entities/cat.entity.ts import { Entity, Column } from 'typeorm'; import { TenantEntity } from '../../common/entities/tenant.entity'; @Entity() export class Cat extends TenantEntity { @Column() name: string; @Column() age: number; @Column() breed: string; }
2.2 用TypeORM订阅器自动注入companyId
监听实体的新增/更新事件,自动填充companyId并防止篡改:
// src/common/subscribers/tenant.subscriber.ts import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'; import { Injectable, ForbiddenException } from '@nestjs/common'; import { TenantEntity } from '../entities/tenant.entity'; import { TenantContextService } from '../context/tenant.context'; @Injectable() @EventSubscriber() export class TenantSubscriber implements EntitySubscriberInterface<TenantEntity> { constructor(private readonly tenantContextService: TenantContextService) {} listenTo() { return TenantEntity; } beforeInsert(event: InsertEvent<TenantEntity>) { const companyId = this.tenantContextService.getCompanyId(); event.entity.companyId = companyId; } beforeUpdate(event: UpdateEvent<TenantEntity>) { const companyId = this.tenantContextService.getCompanyId(); // 防止更新时篡改租户ID if (event.entity?.companyId && event.entity.companyId !== companyId) { throw new ForbiddenException('无权修改其他租户数据'); } event.entity.companyId = companyId; } }
2.3 自定义仓库自动添加查询过滤
重写TypeORM仓库的核心方法,自动拼接companyId过滤条件:
// src/common/repositories/tenant.repository.ts import { Repository, SelectQueryBuilder, EntityTarget, EntityManager, QueryRunner, FindOneOptions, FindManyOptions } from 'typeorm'; import { Injectable } from '@nestjs/common'; import { TenantEntity } from '../entities/tenant.entity'; import { TenantContextService } from '../context/tenant.context'; @Injectable() export class TenantRepository<T extends TenantEntity> extends Repository<T> { constructor( target: EntityTarget<T>, manager: EntityManager, queryRunner?: QueryRunner, private readonly tenantContextService: TenantContextService, ) { super(target, manager, queryRunner); } createQueryBuilder(alias?: string, queryRunner?: QueryRunner): SelectQueryBuilder<T> { const qb = super.createQueryBuilder(alias, queryRunner); const companyId = this.tenantContextService.getCompanyId(); qb.where(`${alias}.companyId = :companyId`, { companyId }); return qb; } async findOne(options?: FindOneOptions<T>): Promise<T | null> { const companyId = this.tenantContextService.getCompanyId(); return super.findOne({ ...options, where: { ...options?.where, companyId }, }); } async find(options?: FindManyOptions<T>): Promise<T[]> { const companyId = this.tenantContextService.getCompanyId(); return super.find({ ...options, where: { ...options?.where, companyId }, }); } }
在实体模块中使用自定义仓库:
// src/cats/cats.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; import { Cat } from './entities/cat.entity'; import { TenantRepository } from '../common/repositories/tenant.repository'; import { TenantContextService } from '../common/context/tenant.context'; @Module({ imports: [ TypeOrmModule.forFeature([ { name: Cat.name, useFactory: (dataSource) => new TenantRepository(Cat, dataSource.manager, dataSource.createQueryRunner(), dataSource.getRepository(TenantContextService)), }, ]), ], controllers: [CatsController], providers: [CatsService], }) export class CatsModule {}
步骤3:简化控制器与服务层
现在控制器和服务完全不需要手动处理companyId:
控制器代码
// src/cats/cats.controller.ts import { Controller, Post, Body, Get } from '@nestjs/common'; import { CatsService } from './cats.service'; import { CreateCatDto } from './dto/create-cat.dto'; @Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) {} @Post() create(@Body() createCatDto: CreateCatDto) { return this.catsService.create(createCatDto); } @Get() findAll() { return this.catsService.findAll(); } }
服务代码
// src/cats/cats.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Cat } from './entities/cat.entity'; import { TenantRepository } from '../common/repositories/tenant.repository'; import { CreateCatDto } from './dto/create-cat.dto'; @Injectable() export class CatsService { constructor( @InjectRepository(Cat) private readonly catRepository: TenantRepository<Cat>, ) {} create(createCatDto: CreateCatDto) { return this.catRepository.save(createCatDto); } findAll() { return this.catRepository.find(); } }
关键优势
- 零样板代码:无需在控制器、服务的每个方法中传递
companyId - 强隔离保障:所有数据操作自动带上租户过滤,避免人为疏漏
- 性能友好:使用Node.js原生AsyncLocalStorage,性能远优于请求作用域的服务/控制器
- 扩展性强:新增租户实体只需继承基类,无需额外修改业务代码
内容的提问来源于stack exchange,提问作者jmoyson
相关产品推荐
相关产品推荐

