我想连接到基于子域(多租户)的任何数据库,但是我不确定该怎么做,因为我的数据库具有以下代码。
我的代码在启动应用程序时运行,但是我不知道如何更改基于子域的数据源,请帮助我,谢谢。
PD:我在每个请求中都创建了中间件,但是我不知道如何更改源。
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);
}
},
}
];
我想在子域(多租户)的每个请求中更改emy数据源
答案 0 :(得分:12)
这是我和猫鼬一起使用的解决方案
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
。请求标头提取方法如下所示。 如果要提取子域,则可以通过调用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();
}
}
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数据库路由。在这种情况下,所有user
路由都是租户的一部分,并将由租户db服务。@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({});
}
}
答案 1 :(得分:0)
我们还为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 });
}
这就是服务在我们的应用程序中的样子。
希望这会激发您的灵感,并帮助您解决问题:)