Hir*_*ios 4 multi-tenant mongodb nestjs
我想连接到基于子域(多租户)的任何数据库,但我不知道该怎么做。
我的代码在应用程序启动时运行,但我不知道如何根据子域更改数据源。
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);
}
},
}
];
Run Code Online (Sandbox Code Playgroud)
我想根据子域(多租户)在每个请求中更改我的数据源
San*_*air 14
这是我与猫鼬一起使用的解决方案
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 });
}
}
Run Code Online (Sandbox Code Playgroud)
TenantAwareMiddleware中间件tenant id从请求上下文中获取。您可以在此处创建自己的逻辑以tenant id从请求标头或请求 url 子域中提取。请求头提取方法如下所示。如果你想提取子域,同样可以Request通过调用从对象中提取它来完成req.subdomains,这会给你一个子域列表,然后你可以从中获得你正在寻找的。
@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();
}
}
Run Code Online (Sandbox Code Playgroud)
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'));
}
}
Run Code Online (Sandbox Code Playgroud)
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],
},
];
Run Code Online (Sandbox Code Playgroud)
TenantsModule- 在这里您可以看到TenantConnectionFactory添加为提供者并被导出以在其他模块中使用。@Module({
imports: [
CoreModule,
],
controllers: [TenantsController],
providers: [
TenantsService,
TenantConnection,
...TenantConnectionFactory,
],
exports: [
...TenantConnectionFactory,
],
})
export class TenantsModule {}
Run Code Online (Sandbox Code Playgroud)
TenantModelProviders - 由于您的租户模型取决于租户连接,您的模型必须通过提供者定义,然后包含在您初始化它们的模块中。export const TenantModelProviders = [
{
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['TENANT_CONNECTION'],
},
];
Run Code Online (Sandbox Code Playgroud)
UsersModule- 本课程将使用模型。您还可以看到这里配置的中间件对您的租户数据库路由起作用。在这种情况下,所有user路由都是租户的一部分,将由租户 db 提供服务。@Module({
imports: [
CoreModule,
TenantsModule,
],
providers: [
UsersService,
...TenantModelProviders,
],
controllers: [UsersController],
})
export class UsersModule implements NestModule {
configure(context: MiddlewareConsumer) {
context.apply(TenantAwareMiddleware).forRoutes('/users');
}
}
Run Code Online (Sandbox Code Playgroud)
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({});
}
}
Run Code Online (Sandbox Code Playgroud)
我们还为 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('*');
}
Run Code Online (Sandbox Code Playgroud)
这是我们中间件的一个例子。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 });
}
Run Code Online (Sandbox Code Playgroud)
这就是我们的应用程序中的服务的样子。
希望这会激励您并帮助您解决问题:)
| 归档时间: |
|
| 查看次数: |
6673 次 |
| 最近记录: |