我如何在 NESTJS 中设置多租户
Posted
技术标签:
【中文标题】我如何在 NESTJS 中设置多租户【英文标题】:How can i setup multitenant in NESTJS 【发布时间】:2019-09-28 20:11:43 【问题描述】:我想连接到基于子域(多租户)的任何数据库,但我不知道该怎么做。
我的代码在应用启动时运行,但我不知道如何根据子域更改数据源。
PS:我为每个请求创建了中间件,但我不知道如何更改源。
我的数据库有以下代码:
import connect, createConnection from 'mongoose';
import SERVER_CONFIG, DB_CONNECTION_TOKEN from '../server.constants';
const opts =
useCreateIndex: true,
useNewUrlParser: true,
keepAlive: true,
socketTimeoutMS: 30000,
poolSize: 100,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
autoReconnect: true,
;
export const databaseProviders = [
provide: DB_CONNECTION_TOKEN,
useFactory: async () =>
try
console.log(`Connecting to $ SERVER_CONFIG.db `);
return await createConnection(`$SERVER_CONFIG.db`, opts);
catch (ex)
console.log(ex);
,
];
我想根据子域(多租户)更改每个请求中的数据源
【问题讨论】:
【参考方案1】:这是我与猫鼬一起使用的解决方案
TenantsService
用于管理应用中的所有租户
@Injectable()
export class TenantsService
constructor(
@InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
)
/**
* Save tenant data
*
* @param CreateTenantDto createTenant
* @returns Promise<ITenant>
* @memberof TenantsService
*/
async create(createTenant: CreateTenantDto): Promise<ITenant>
try
const dataToPersist = new this.tenantModel(createTenant);
// Persist the data
return await dataToPersist.save();
catch (error)
throw new HttpException(error, HttpStatus.BAD_REQUEST);
/**
* Find details of a tenant by name
*
* @param string name
* @returns Promise<ITenant>
* @memberof TenantsService
*/
async findByName(name: string): Promise<ITenant>
return await this.tenantModel.findOne( name );
TenantAwareMiddleware
中间件从请求上下文中获取 tenant id
。您可以在此处创建自己的逻辑以从请求标头或请求 url 子域中提取 tenant id
。请求头提取方法如下所示。
如果您想提取子域,也可以通过调用req.subdomains
从Request
对象中提取子域来完成,这将为您提供子域列表,然后您可以从中获取您正在寻找的子域那个。
@Injectable()
export class TenantAwareMiddleware implements NestMiddleware
async use(req: Request, res: Response, next: NextFunction)
// Extract from the request object
const subdomains, headers = req;
// Get the tenant id from header
const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];
if (!tenantId)
throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
// Set the tenant id in the header
req['tenantId'] = tenantId.toString();
next();
TenantConnection
此类用于使用 tenant id
创建新连接,如果现有连接可用,它将返回相同的连接(以避免创建额外的连接)。
@Injectable()
export class TenantConnection
private _tenantId: string;
constructor(
private tenantService: TenantsService,
private configService: ConfigService,
)
/**
* Set the context of the tenant
*
* @memberof TenantConnection
*/
set tenantId(tenantId: string)
this._tenantId = tenantId;
/**
* Get the connection details
*
* @param ITenant tenant
* @returns
* @memberof TenantConnection
*/
async getConnection(): Connection
// Get the tenant details from the database
const tenant = await this.tenantService.findByName(this._tenantId);
// Validation check if tenant exist
if (!tenant)
throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
// Get the underlying mongoose connections
const connections: Connection[] = mongoose.connections;
// Find existing connection
const foundConn = connections.find((con: Connection) =>
return con.name === `tenantDB_$tenant.name`;
);
// Check if connection exist and is ready to execute
if (foundConn && foundConn.readyState === 1)
return foundConn;
// Create a new connection
return await this.createConnection(tenant);
/**
* Create new connection
*
* @private
* @param ITenant tenant
* @returns Connection
* @memberof TenantConnection
*/
private async createConnection(tenant: ITenant): Promise<Connection>
// Create or Return a mongo connection
return await mongoose.createConnection(`$tenant.uri`, this.configService.get('tenant.dbOptions'));
TenantConnectionFactory
这是自定义提供程序,可为您提供 tenant id
并帮助创建连接
// Tenant creation factory
export const TenantConnectionFactory = [
provide: 'TENANT_CONTEXT',
scope: Scope.REQUEST,
inject: [REQUEST],
useFactory: (req: Request): ITenantContext =>
const tenantId = req as any;
return new TenantContext(tenantId);
,
,
provide: 'TENANT_CONNECTION',
useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose> =>
// Set tenant context
connection.tenantId = context.tenantId;
// Return the connection
return connection.getConnection();
,
inject: ['TENANT_CONTEXT', TenantConnection],
,
];
TenantsModule
- 在这里您可以看到作为提供程序添加的TenantConnectionFactory
,并且正在导出以在其他模块中使用。
@Module(
imports: [
CoreModule,
],
controllers: [TenantsController],
providers: [
TenantsService,
TenantConnection,
...TenantConnectionFactory,
],
exports: [
...TenantConnectionFactory,
],
)
export class TenantsModule
TenantModelProviders
- 由于您的租户模型依赖于租户连接,因此您的模型必须通过提供程序定义,然后包含在您初始化它们的模块中。
export const TenantModelProviders = [
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['TENANT_CONNECTION'],
,
];
UsersModule
- 此类将使用模型。您还可以看到此处配置的中间件以作用于您的 Tenand db 路由。这种情况下,所有user
路由都是租户的一部分,将由租户数据库提供服务。
@Module(
imports: [
CoreModule,
TenantsModule,
],
providers: [
UsersService,
...TenantModelProviders,
],
controllers: [UsersController],
)
export class UsersModule implements NestModule
configure(context: MiddlewareConsumer)
context.apply(TenantAwareMiddleware).forRoutes('/users');
UsersService
- 从用户模块访问租户数据库的示例实现
@Injectable()
export class UsersService
constructor(
@Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
@Inject('USER_MODEL') private userModel: Model<IUser>,
)
Logger.debug(`Current tenant: $this.tenantContext.tenantId`);
/**
* Create a new user
*
* @param CreateUserDto user
* @returns Promise<IUser>
* @memberof UsersService
*/
async create(user: CreateUserDto): Promise<IUser>
try
const dataToPersist = new this.userModel(user);
// Persist the data
return await dataToPersist.save();
catch (error)
throw new HttpException(error, HttpStatus.BAD_REQUEST);
/**
* Get the list of all users
*
* @returns Promise<IUser>
* @memberof UsersService
*/
async findAll(): Promise<IUser>
return await this.userModel.find();
【讨论】:
使用范围请求会导致所有使用工厂的服务的性能发生变化?难道没有不涉及再次实例化服务的解决方案吗? @ontimond 我想你可以,但你使用的策略只会改变你获取租户 ID 的方式。发布连接创建过程将保持不变。【参考方案2】:我们的 NestJS 设置也有一个多租户设置。 您可以有一个中间件,根据请求决定使用哪个数据源。在我们的示例中,我们使用了 TypeORM,它在 NestJS 中有很好的集成。 TypeORM 包中有一些有用的功能。
中间件
export class AppModule
constructor(private readonly connection: Connection)
configure(consumer: MiddlewareConsumer): void
consumer
.apply(async (req, res, next) =>
try
getConnection(tenant);
next();
catch (e)
const tenantRepository = this.connection.getRepository(tenant);
const tenant = await tenantRepository.findOne( name: tenant );
if (tenant)
const createdConnection: Connection = await createConnection(options);
if (createdConnection)
next();
else
throw new CustomNotFoundException(
'Database Connection Error',
'There is a Error with the Database!',
);
).forRoutes('*');
这是我们中间件的一个例子。 TypeORM 在内部管理连接。因此,您首先要尝试为该特定租户加载连接。如果有,那就好,否则就创建一个。这里的好处是,一旦创建连接,在 TypeORM 连接管理器中仍然可用。这样,您始终可以在路线中建立联系。 在您的路线中,您需要为您的租户提供身份证明。在我们的例子中,它只是一个从 url 中提取的字符串。无论它是什么值,您都可以将其绑定到中间件中的请求对象。在您的控制器中,您再次提取该值并将其传递给您的服务。然后,您必须为您的租户加载存储库,然后一切顺利。
服务类
@Injectable()
export class SampleService
constructor()
async getTenantRepository(tenant: string): Promise<Repository<Entity>>
try
const connection: Connection = await getConnection(tenant);
return connection.getRepository(Property);
catch (e)
throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
async findOne(params: Dto, tenant: string)
const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);
return await propertyRepository.findOne( where: params );
这就是我们应用程序中服务的样子。
希望这会激励你,让你解决问题:)
【讨论】:
我希望看到它的完整 GIT 存储库以及 getConnection 方法的示例以及 MiddlewareConsumer。【参考方案3】:您应该使用带有工厂的提供程序在每个服务中注入连接详细信息,并相应地切换存储库。
这里是连接工厂(假设请求包含租户 id):
const connectionFactory =
provide: CONNECTION,
scope: Scope.REQUEST,
useFactory: (request: ExpressRequest) =>
const tenantId = request;
if (tenantId)
return getTenantConnection(tenantId);
return null;
,
inject: [REQUEST],
;
每个租户的连接可以这样得到:
export function getTenantConnection(tenantId: string): Promise<Connection>
const connectionName = `tenant_$tenantId`;
const connectionManager = getConnectionManager();
if (connectionManager.has(connectionName))
const connection = connectionManager.get(connectionName);
return Promise.resolve(connection.isConnected ? connection : connection.connect());
return createConnection(
...(tenantsOrmconfig as PostgresConnectionOptions),
name: connectionName,
schema: connectionName,
);
然后就可以在每个服务中使用连接了:
@Injectable()
export class CatsService
private readonly catsRepository: Repository<Cat>;
constructor(
@Inject(CONNECTION) connection: Connection,
)
this.catsRepository = connection.getRepository(Cat);
create(createCatDto: CreateCatDto): Promise<Cat>
const cat = new Cat();
cat.name = createCatDto.name;
return this.catsRepository.save(cat);
async findAll(): Promise<Cat[]>
return this.catsRepository.find();
请注意,本文提供了一个完整的存储库,其中包含所有相关的服务和设置,它进行了演练; https://thomasvds.com/schema-based-multitenancy-with-nest-js-type-orm-and-postgres-sql/.
【讨论】:
以上是关于我如何在 NESTJS 中设置多租户的主要内容,如果未能解决你的问题,请参考以下文章