如何在NestJS+TypeORM+PostgreSQL中通过DataSource实现运行时动态切换多租户数据源?
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
poolSizein your baseDataSourceOptionsbased on expected tenant count and traffic to avoid exhausting database connections. - Caching: The
dataSourcesmap 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

