You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NestJS/TypeORM中实现基于列的多租户且无需传递companyId?

基于NestJS + TypeORM的无样板多租户实现(共享库/共享Schema)

针对你提出的问题,我们可以通过AsyncLocalStorage(ALS)存储请求上下文 + TypeORM订阅器/自定义仓库的组合方案,彻底消除手动传递companyId的样板代码,同时避免请求作用域带来的性能损耗。

核心思路

  1. 从请求中提取companyId,存入Node.js原生的AsyncLocalStorage,让整个请求链路都能安全获取该值
  2. 在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();
  }
}

关键优势

  1. 零样板代码:无需在控制器、服务的每个方法中传递companyId
  2. 强隔离保障:所有数据操作自动带上租户过滤,避免人为疏漏
  3. 性能友好:使用Node.js原生AsyncLocalStorage,性能远优于请求作用域的服务/控制器
  4. 扩展性强:新增租户实体只需继承基类,无需额外修改业务代码

内容的提问来源于stack exchange,提问作者jmoyson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 13:25:17