我们正在 GitHub 问题 loopback-next#5056 中讨论如何实现租户隔离的不同方法。我们还提供了一个示例多租户应用程序,请参阅其README。拉取请求loopback-next#5681 正在实现一个通用池服务,该服务也可用于实现租户隔离。
恐怕你的问题没有提供足够的细节让我给你一个具体的解决方案,所以我将引用我的Datasource-based tenant isolation提案中的代码sn-ps。
为了方便注入特定于租户的数据源,让我们保持相同的数据源名称(绑定键),例如datasources.tenantData,但实现数据源值的动态解析。想法是将lb4 datasource 搭建的数据源类改造成Provider 类。
import {inject} from '@loopback/core';
import {juggler} from '@loopback/repository';
const config = {
name: 'tenantData',
connector: 'postgresql',
// ...
};
export class TenantDataSourceProvider implements Provider<TenantDataSource > {
constructor(
@inject('datasources.config.tenant', {optional: true})
private dsConfig: object = config,
@inject(SecurityBindings.USER)
private currentUser: UserProfile,
) {}
value() {
const config = {
...this.dsConfig,
// apply tenant-specific settings
schema: this.currentUser.name
};
// Because we are using the same binding key for multiple datasource instances,
// we need to implement our own caching behavior to support SINGLETON scope
// I am leaving this aspect as something to figure out as part of the research
const cached = // look up existing DS instance
if (cached) return cached;
const ds = new TenantDataSource(config);
// store the instance in the cache
return ds;
}
}
export class TenantDataSource extends juggler.DataSource {
static dataSourceName = 'tenant';
// constructor is not needed, we can use the inherited one.
// start/stop methods are needed, I am skipping them for brevity
}
有不同的方法可以实现对每个租户数据源的缓存。理想情况下,我想为此重用 Context 。事实证明这很简单!
我们希望每个租户数据源都有自己的数据源名称和绑定键。为了允许存储库通过@inject 获取数据源,我们可以实现一个“代理”数据源提供程序,它将使用名称数据源之一进行解析。
export class TenantDataSourceProvider implements Provider<TenantDataSource> {
private dataSourceName: string;
private bindingKey: string;
constructor(
@inject('datasources.config.tenant', {optional: true})
private dsConfig: object = config,
@inject(SecurityBindings.USER)
private currentUser: UserProfile,
@inject.context()
private currentContext: Context,
@inject(CoreBindings.APPLICATION_INSTANCE)
private app: Application,
) {
this.dataSourceName = `tenant-${this.currentUser.name}`;
this.bindingKey = `datasources.${this.dataSourceName}`;
}
value() {
if (!this.currentContext.isBound(this.bindingKey)) {
this.setupDataSource();
}
return this.currentContext.get<juggler.DataSource>(this.bindingKey);
}
private setupDataSource() {
const resolvedConfig = {
...this.dsConfig,
// apply tenant-specific settings
schema: this.currentUser.name,
};
const ds = new TenantDataSource(resolvedConfig);
// Important! We need to bind the datasource to the root (application-level)
// context to reuse the same datasource instance for all requests.
this.app.bind(this.bindingKey).to(ds).tag({
name: this.dataSourceName,
type: 'datasource',
namespace: 'datasources',
});
}
}
export class TenantDataSource extends juggler.DataSource {
// no static members like `dataSourceName`
// constructor is not needed, we can use the inherited one.
// start/stop methods are needed, I am skipping them for brevity
}
上面的代码示例在每个租户发出第一个请求时自动创建每个租户的数据源。在大多数租户仅不经常连接使用该应用程序的情况下,这应该可以提供更快的应用程序启动并可能减少对数据库的压力。另一方面,特定于租户的数据库连接的任何问题只有在发出第一个请求后才会被发现,这可能为时已晚。如果您希望在启动时立即建立(并检查)所有租户数据库连接,您可以将代码从 setupDataSource 移动到启动脚本并为每个已知租户调用它。
另见the following comment:
由于this.bindingKeys 是动态生成的,介意分享这些数据源是如何注入存储库的吗?
想法是将静态数据源键绑定到TenantDataSourceProvider,这将解析为动态创建的数据源之一。
例如在应用构造函数中:
this.bind('datasources.tenant').toProvider(TenantDataSourceProvider);
然后你就可以像往常一样注入数据源了,例如:
@inject('datasources.tenant')
dataSource: TenantDataSource