【问题标题】:Link single repo to Multiple Datasources -- Loopback 4将单个 repo 链接到多个数据源——Loopback 4
【发布时间】:2020-06-11 15:44:32
【问题描述】:

我们在 SaaS 应用程序中使用 Loopback 4。我们被困在一个案例中。

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

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

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

【问题讨论】:

    标签: loopback4


    【解决方案1】:

    我们正在 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
    

    【讨论】:

      猜你喜欢
      • 2016-05-19
      • 1970-01-01
      • 1970-01-01
      • 2016-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-02
      • 1970-01-01
      相关资源
      最近更新 更多