Link 单个回购到多个数据源 -- Loopback 4

Link single repo to Multiple Datasources -- Loopback 4

我们在 SaaS 应用程序中使用 Loopback 4。我们陷入了一种情况。

我们正在努力让每个用户都有自己独立的数据库。因此,当用户登录应用程序时,我们希望创建一个动态数据源,这就是我们所做的。但问题是如何 link 存储库到动态创建的数据源。

我们尝试的一个解决方案是根据每个用户通过 controller 请求更改存储库中的 this.datasource,但是当多个用户同时请求时,datasource 值会更改. 这是一个卑微的请求,请帮助我们。

我知道我可能没有解释正确。

我们正在讨论如何在 GitHub 问题中实现租户隔离的不同方式 loopback-next#5056. We also provide an example multi-tenant application, see its README. The pull request loopback-next#5681 正在实现一个通用池服务,该服务也可用于实现租户隔离。

恐怕你的问题没有提供足够的细节让我给你一个具体的解决方案,所以我将引用我的 Datasource-based tenant isolation 提案中的代码片段。


为了便于注入租户特定的数据源,让我们保持相同的数据源名称(绑定键),例如datasources.tenantData,但实现数据源值的动态解析。这个想法是将 lb4 datasource 搭建的数据源 class 重新加工成 Provider class.

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
}

实现每个租户数据源缓存的方法有多种。理想情况下,我想为此重用上下文。原来这很简单!

我们希望每个租户数据源都有自己的数据源名称和绑定键。为了允许存储库通过 @inject 获取数据源,我们可以实现一个 "proxy" 数据源提供程序,它将使用名称数据源之一进行解析。

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:

Mind sharing how these data sources are injected into repositories since this.bindingKeys are being dynamically generated?

想法是将静态数据源键绑定到 TenantDataSourceProvider,它将解析为动态创建的数据源之一。

例如,在应用构造函数中:

this.bind('datasources.tenant').toProvider(TenantDataSourceProvider);

然后你可以用通常的方式注入数据源,例如:

@inject('datasources.tenant')
dataSource: TenantDataSource