Rails / Multi-Tenancy:基于不同模型的db值/全局设置的条件默认范围?

时间:2017-07-07 13:34:09

标签: ruby-on-rails multi-tenant scopes

我有一个多租户的Rails应用程序。每个模型都有一个account_id,属于一个帐户,并且具有当前帐户ID的默认范围:

class Derp < ApplicationRecord
  default_scope { where(account_id: Account.current_id) }
  belongs_to :account
end

这很好用,我在其他应用程序的生产中使用了这种模式(我知道默认范围是不受欢迎的,但这是一种公认​​的模式。请参阅:https://leanpub.com/multi-tenancy-rails)。

现在这里是踢球者 - 我有一个客户端(可能还有更多的线路,谁知道),谁想在自己的服务器上运行软件。为了解决这个问题,我简单地创建了一个带有type属性的服务器模型:

class Server < ApplicationRecord
  enum server_type: { multitenant: 0, standalone: 1 }
end

现在在我的多租户服务器实例上,我只创建一个Server记录并将server_type设置为0,并在我的独立实例上将其设置为1.然后我在应用程序控制器中有一些帮助方法来帮助有了这个,即:

class ApplicationController < ActionController::Base
  around_action :scope_current_account

  ...

  def server
    @server ||= Server.first
  end

  def current_account
    if server.standalone?
      @current_account ||= Account.first
    elsif server.first.multitenant?
      @current_account ||= Account.find_by_subdomain(subdomain) if subdomain
    end
  end

  def scope_current_account
    Account.current_id = current_account.id
    yield
  rescue ActiveRecord::RecordNotFound
    redirect_to not_found_path
  ensure
    Account.current_id = nil 
  end
end

这有效,但是我有大量的记录集,我在这个特定的独立客户端上查询(70,000条记录)。我在account_id上有一个索引,但我的主要客户表在我的开发机器上从100ms到400ms。

然后我意识到:独立服务器根本不需要关注帐户ID,特别是如果它会影响性能。

所以我真的要做的就是让这条线有条件:

default_scope { where(account_id: Account.current_id) }

我想做这样的事情:

class Derp < ApplicationRecord
  if Server.first.multitenant?
    default_scope { where(account_id: Account.current_id) }
  end
end

但显然语法错了。我已经在条件范围的Stack Overflow上看到了一些其他示例,但似乎没有一个基于完全独立的模型的条件语句。有没有办法在Ruby中实现类似的东西?

编辑:Kicker,我刚刚意识到这只会解决一个独立服务器的速度问题,并且所有多租户帐户仍然需要处理使用account_id进行查询。也许我应该专注于那个......

2 个答案:

答案 0 :(得分:1)

我会避免使用y因为我过去曾被它咬过。特别是,我在一个应用程序中有一些地方,我想肯定有它的范围,以及其他我不喜欢的地方。我希望范围界定的地方通常最终成为控制器/后台工作,而我不想要/需要它的地方最终成为测试。

因此,考虑到这一点,我会选择控制器中的显式方法,而不是模型中的隐式作用域:

你有:

default_scope

我在控制器中有一个名为class Derp < ApplicationRecord if Server.first.multitenant? default_scope { where(account_id: Account.current_id) } end end 的方法:

account_derps

然后,只要我想加载给定帐户的derps,我就会使用def account_derps Derp.for_account(current_account) end 。如果我需要,我可以自由地使用account_derps进行无范围的查找。

关于这种方法的最佳部分是你可以在这里放弃你的Derp逻辑。

你在这里提到另一个问题:

  

这样可行,但我已经获得了大量的记录集,我在这个特定的独立客户端上查询(70,000条记录)。我在account_id上有一个索引,但我的主要客户表在我的开发机器上从100毫秒到400毫秒。

我认为这很可能是由于缺少索引。但我在这里或查询中看不到表格架构,所以我不确定。可能是您在Server.first.multitenant?和其他某个字段上执行了查询,但您只是将索引添加到account_id。如果您正在使用PostgreSQL,那么查询前的account_id将指向正确的方向。如果您不确定如何破译其结果(有时它们可​​能很棘手),那么我建议使用精彩的pev(Postgres EXPLAIN Visualizer),它会指向查询中最慢的部分以图形格式。

最后,感谢您花时间阅读我的书,并就SO的相关主题提出如此详细的问题:)

答案 1 :(得分:0)

这是我的解决方案:

首先,将任何帐户作用域模型所具有的帐户范围内容抽象为从ApplicationRecord继承的抽象基类:

class AccountScopedRecord < ApplicationRecord
  self.abstract_class = true

  default_scope { where(account_id: Account.current_id) }

  belongs_to :account
end

现在任何模型都可以干净利落地像帐户一样:

class Job < AccountScopedRecord
  ...
end

要解决有条件的抽象问题,请进一步了解ActiveRecord问题:

module AccountScoped
  extend ActiveSupport::Concern

  included do
    default_scope { where(account_id: Account.current_id) }
    belongs_to :account
  end
end

然后AccountScopedRecord可以:

class AccountScopedRecord < ApplicationRecord
  self.abstract_class = true

  if Server.first.multitenant?
    send(:include, AccountScoped)
  end
end

现在,独立帐户可以忽略任何与帐户相关的内容:

# Don't need this callback on standalone anymore
around_action :scope_current_account, if: multitenant?

# Method gets simplified
def current_account
  @current_account ||= Account.find_by_subdomain(subdomain) if subdomain
end