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

如何在NestJS+TypeORM+PostgreSQL中通过DataSource实现运行时动态切换多租户数据源?

Dynamic Tenant Database Connections with NestJS, TypeORM (DataSource API) and PostgreSQL

Absolutely! The newer DataSource API in TypeORM fully supports dynamic, named connections—perfect for your multi-tenant setup where each tenant has an isolated but structurally identical database. Let’s walk through how to implement this, replacing the deprecated Connection/getConnectionManager approach, and integrate it with your @nestjs/passport guards.

Core Concept: Manage a Pool of Named DataSources

Instead of a single static connection, we’ll maintain a cache of DataSource instances, each keyed by your tenant’s name. When a user logs in, we’ll fetch or create the corresponding DataSource for their tenant and use it for all subsequent database operations in their request context.

Step 1: Create a Tenant DataSource Manager Service

First, build a service to handle creating, caching, and retrieving DataSource instances per tenant. This acts as the replacement for the old getConnectionManager.

import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { DataSource, DataSourceOptions } from 'typeorm';
import { YourSharedEntity1, YourSharedEntity2 } from './entities'; // Import your shared entities

@Injectable()
export class TenantDataSourceService implements OnModuleDestroy {
  private readonly dataSources = new Map<string, DataSource>();

  // Base configuration shared across all tenants
  private getBaseOptions(): Omit<DataSourceOptions, 'database'> {
    return {
      type: 'postgres',
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT),
      username: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
      entities: [YourSharedEntity1, YourSharedEntity2], // Reuse your identical entity classes
      synchronize: false, // Disable in production; use migrations instead
      logging: process.env.NODE_ENV === 'development',
    };
  }

  // Get or create a DataSource for a specific tenant
  async getTenantDataSource(tenantName: string): Promise<DataSource> {
    if (this.dataSources.has(tenantName)) {
      return this.dataSources.get(tenantName);
    }

    // Build tenant-specific DataSource config
    const dataSourceOptions: DataSourceOptions = {
      ...this.getBaseOptions(),
      database: tenantName, // Assume tenant name matches your database name
    };

    const dataSource = new DataSource(dataSourceOptions);
    await dataSource.initialize();

    this.dataSources.set(tenantName, dataSource);
    return dataSource;
  }

  // Clean up all connections when the module shuts down
  async onModuleDestroy() {
    await Promise.all(
      Array.from(this.dataSources.values()).map((ds) => ds.destroy()),
    );
  }
}

Step 2: Integrate with Passport Guards to Fetch Tenant Context

Modify your auth guard to extract the tenant identifier (from JWT, request headers, or session) and attach it to the request object. This lets downstream services know which tenant’s DataSource to use.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(err, user, info, context: any) {
    const request: Request = context.switchToHttp().getRequest();
    // Extract tenant name from your JWT payload (adjust based on your token structure)
    request.tenantName = user.tenantName; 
    return super.handleRequest(err, user, info, context);
  }
}

Step 3: Use the Tenant DataSource in Your Services

Inject the TenantDataSourceService into your business logic services, fetch the tenant’s DataSource from the request context, and use it to access repositories.

import { Injectable, Request } from '@nestjs/common';
import { TenantDataSourceService } from './tenant-data-source.service';
import { YourSharedEntity1 } from './entities';

@Injectable()
export class TenantService {
  constructor(private readonly tenantDataSourceService: TenantDataSourceService) {}

  async getEntityData(@Request() req: any): Promise<YourSharedEntity1[]> {
    const dataSource = await this.tenantDataSourceService.getTenantDataSource(req.tenantName);
    const repository = dataSource.getRepository(YourSharedEntity1);
    return repository.find();
  }
}

Step 4: (Optional) Request-Scoped Provider for Convenience

To avoid passing the request context everywhere, create a request-scoped provider that automatically fetches the correct DataSource for the current tenant:

import { Provider, Scope } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { TenantDataSourceService } from './tenant-data-source.service';

export const TenantDataSourceProvider: Provider = {
  provide: 'TENANT_DATASOURCE',
  scope: Scope.REQUEST,
  useFactory: async (req: any, tenantDsService: TenantDataSourceService) => {
    return tenantDsService.getTenantDataSource(req.tenantName);
  },
  inject: ['REQUEST', TenantDataSourceService],
};

Then inject it directly into your services:

constructor(@Inject('TENANT_DATASOURCE') private readonly dataSource: DataSource) {}

Key Considerations

  • Connection Pooling: Adjust the poolSize in your base DataSourceOptions based on expected tenant count and traffic to avoid exhausting database connections.
  • Caching: The dataSources map prevents reinitializing connections for the same tenant. Add a cleanup mechanism for inactive tenants if needed.
  • Migrations: Run migrations against all tenant databases by looping through tenant names and executing migrations via each DataSource.
  • Error Handling: Add try/catch blocks around dataSource.initialize() to handle cases where a tenant database doesn’t exist or credentials are invalid.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 00:22:35