问题描述
我想连接到基于子域(多租户)的任何数据库,但是我不确定该怎么做.
I want to connect to any database based on the subdomain (multi-tenant), but i'm not sure how can i do it.
我的代码在启动应用程序时运行,但是我不知道如何基于子域更改数据源.
My code runs when the app is started, but i don't know how to change the Datasource based on subdomain.
PS:我为每个请求创建了中间件,但是我不知道如何更改源.
PS: I created middleware on each request, but I don't know how to change the source.
我的数据库有以下代码:
I have the following code for my DB:
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);
}
},
}
];
我想基于子域(多租户)更改每个请求中的数据源
I want to change my datasource in each request based on subdomain (multi-tenant)
推荐答案
这是我与猫鼬一起使用的解决方案
Here is a solution that i used with mongoose
-
TenantsService
用于管理应用程序中的所有租户
TenantsService
used to manage all tenants in the application
@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
.请求标头提取方法如下所示.
TenantAwareMiddleware
middleware to get thetenant id
from the request context. You can make your own logic here to extract thetenant id
, either from request header or from request url subdomain. Request header extraction method is shown here.
如果要提取子域,则可以通过调用req.subdomains
从Request
对象中提取子域来完成,这将为您提供子域列表,然后您可以从中找到所需的子域.那个.
If you want to extract the subdomain the same can be done by extracting it from the Request
object by calling req.subdomains
, which would give you a list of subdomains and then you can get the one you are looking for from that.
@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
创建新连接,并且如果有可用的现有连接,它将返回相同的连接(以避免创建其他连接).
TenantConnection
this class is used to create new connection usingtenant id
and if there is an existing connection available it would return back the same connection (to avoid creating additional connections).
@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
并有助于创建连接
TenantConnectionFactory
this is custom provider which gets you thetenant id
and also helps in creation of the connection
// 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
作为提供者添加并正在导出以便在其他模块中使用.
TenantsModule
- Here you can see theTenantConnectionFactory
added as a provider and is being exported to be used inside other modules.
@Module({
imports: [
CoreModule,
],
controllers: [TenantsController],
providers: [
TenantsService,
TenantConnection,
...TenantConnectionFactory,
],
exports: [
...TenantConnectionFactory,
],
})
export class TenantsModule {}
-
TenantModelProviders
-由于租户模型取决于租户连接,因此必须通过提供程序定义模型,然后将其包含在初始化它们的模块中.
TenantModelProviders
- Since your tenant models depends on the tenant connection, your models have to defined through a provider and then included inside the module where you initialise them.
export const TenantModelProviders = [
{
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['TENANT_CONNECTION'],
},
];
-
UsersModule
-此类将使用模型.您还可以看到此处配置的中间件可以作用于Tenand数据库路由.在这种情况下,所有user
路由都是租户的一部分,并将由租户db服务.
UsersModule
- This class will be using the models. You can also see the middleware being configured here to act upon your tenand db routes. This case all theuser
routes are part of the tenant and will be served by tenant db.
@Module({
imports: [
CoreModule,
TenantsModule,
],
providers: [
UsersService,
...TenantModelProviders,
],
controllers: [UsersController],
})
export class UsersModule implements NestModule {
configure(context: MiddlewareConsumer) {
context.apply(TenantAwareMiddleware).forRoutes('/users');
}
}
-
UsersService
-从用户模块访问租户db的示例实现
UsersService
- Example implementation of accessing tenant db from user module
@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({});
}
}
这篇关于如何在NESTJS中设置多租户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!