Rails/多租户:基于不同模型的数据库值/全局设置的条件默认范围?
Rails / Multi-Tenancy: Conditional default scope based on a different model's db value / global setting?
我有一个 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)。
现在更重要的是 - 我有一个客户(可能还有更多的客户,谁知道呢),他想 运行 在他们自己的服务器上安装该软件。为了解决这个问题,我简单地制作了一个带有类型属性的服务器模型:
class Server < ApplicationRecord
enum server_type: { multitenant: 0, standalone: 1 }
end
现在在我的多租户服务器实例上,我只需创建一个服务器记录并将 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 上有一个索引,但我的主要客户 table 在我的开发机器上用了 100 毫秒到 400 毫秒。
然后我意识到:独立服务器根本不需要关心帐户 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 上看到了一些关于条件范围的其他示例,但是 none 似乎可以使用基于完全独立模型的条件语句。在 Ruby 中有没有办法完成类似的事情?
编辑:我刚刚意识到的问题是,这只会解决一个独立服务器的速度问题,所有多租户帐户仍然需要处理 account_id 的查询。也许我应该专注于此...
这是我的解决方案:
首先,将任何帐户范围模型必须的帐户范围内容抽象为一个从 ApplicationRecord 继承的抽象基础 class:
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
我会避免使用 default_scope
,因为我过去曾被它咬过。特别是,我在应用程序中有一些地方我想 明确地 有它的范围,而其他地方我不想。我想要范围界定的地方通常最终成为控制器/后台作业,而我不想要/不需要它的地方最终成为测试。
考虑到这一点,我会选择控制器中的显式方法,而不是模型中的隐式范围:
而你有:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
我会在控制器中有一个名为 account_derps
:
的方法
def account_derps
Derp.for_account(current_account)
end
然后,无论我想只为给定帐户加载 derps,我都会使用 account_derps
。如果我需要这样做,我可以自由地使用 Derp
进行无范围查找。
这个方法最好的部分是你也可以在这里放弃你的 Server.first.multitenant?
逻辑。
你在这里提到另一个问题:
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
我认为这很可能是因为缺少索引。但是我在这里没有看到 table 架构或查询,所以我不确定。可能是您正在对 account_id
和其他一些字段执行 where 查询,但您只将索引添加到 account_id
。如果您使用的是 PostgreSQL,那么查询前的 EXPLAIN ANALYZE
将为您指明正确的方向。如果您不确定如何破译其结果(有时它们可能很棘手),那么我建议您使用精彩的 pev (Postgres EXPLAIN Visualizer),它会将您指向查询中最慢的部分图形格式。
最后,感谢您花时间阅读我的书,并就 SO 上的相关主题提出如此详细的问题 :)
我有一个 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)。
现在更重要的是 - 我有一个客户(可能还有更多的客户,谁知道呢),他想 运行 在他们自己的服务器上安装该软件。为了解决这个问题,我简单地制作了一个带有类型属性的服务器模型:
class Server < ApplicationRecord
enum server_type: { multitenant: 0, standalone: 1 }
end
现在在我的多租户服务器实例上,我只需创建一个服务器记录并将 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 上有一个索引,但我的主要客户 table 在我的开发机器上用了 100 毫秒到 400 毫秒。
然后我意识到:独立服务器根本不需要关心帐户 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 上看到了一些关于条件范围的其他示例,但是 none 似乎可以使用基于完全独立模型的条件语句。在 Ruby 中有没有办法完成类似的事情?
编辑:我刚刚意识到的问题是,这只会解决一个独立服务器的速度问题,所有多租户帐户仍然需要处理 account_id 的查询。也许我应该专注于此...
这是我的解决方案:
首先,将任何帐户范围模型必须的帐户范围内容抽象为一个从 ApplicationRecord 继承的抽象基础 class:
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
我会避免使用 default_scope
,因为我过去曾被它咬过。特别是,我在应用程序中有一些地方我想 明确地 有它的范围,而其他地方我不想。我想要范围界定的地方通常最终成为控制器/后台作业,而我不想要/不需要它的地方最终成为测试。
考虑到这一点,我会选择控制器中的显式方法,而不是模型中的隐式范围:
而你有:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
我会在控制器中有一个名为 account_derps
:
def account_derps
Derp.for_account(current_account)
end
然后,无论我想只为给定帐户加载 derps,我都会使用 account_derps
。如果我需要这样做,我可以自由地使用 Derp
进行无范围查找。
这个方法最好的部分是你也可以在这里放弃你的 Server.first.multitenant?
逻辑。
你在这里提到另一个问题:
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
我认为这很可能是因为缺少索引。但是我在这里没有看到 table 架构或查询,所以我不确定。可能是您正在对 account_id
和其他一些字段执行 where 查询,但您只将索引添加到 account_id
。如果您使用的是 PostgreSQL,那么查询前的 EXPLAIN ANALYZE
将为您指明正确的方向。如果您不确定如何破译其结果(有时它们可能很棘手),那么我建议您使用精彩的 pev (Postgres EXPLAIN Visualizer),它会将您指向查询中最慢的部分图形格式。
最后,感谢您花时间阅读我的书,并就 SO 上的相关主题提出如此详细的问题 :)