在Rails应用程序中的查询运行时更改表名

Nic*_*k M 1 ruby activerecord ruby-on-rails rails-activerecord

我在Apache + mod_passenger上有一个胖的多租户Rails应用程序,它从PostgreSQL表输出产品价格,如下所示:

Table "public.products"
Column | Type
id     | bigint
name   | character varying(100)
price  | numeric(8,2)
Run Code Online (Sandbox Code Playgroud)

然后在products.rb我有......

class Product < PostgresDatabase
     self.table_name = "products"

     # ... yadda yadda

end
Run Code Online (Sandbox Code Playgroud)

我想要的是以一种非常具体的方式对"产品"表进行分区,这样我最终会得到像每个租户的products_TENANT-ID(基本上是主要产品表的视图,但这是另一个故事)并能够像这样查询:

Products.for_tenant(TENANT-ID).where(:name => "My product")......
Run Code Online (Sandbox Code Playgroud)

我想我可以创建一个方法:

class Product < PostgresDatabase
     self.table_name = "products"

     # ... yadda yadda
     def for_tenant(tid)
          self.table_name = "products_" + tid.to_s
          self
     end
end
Run Code Online (Sandbox Code Playgroud)

但考虑到有大量流量(每秒数千个请求),这会对应用程序产生什么样的影响?有什么我想念的吗?我应该尝试不同的策略吗?

非常感谢您的任何反馈/想法!

Ily*_*hov 5

方法

def self.for_tenant(tid)
  self.table_name = "products_" + tid.to_s
  self
end
Run Code Online (Sandbox Code Playgroud)

但是,它有意义,它有一个副作用:它改变了Product类的表名.稍后在同一请求中使用此类时,例如,以这种方式:

Product.where(name: "My other product") ...
Run Code Online (Sandbox Code Playgroud)

表名不会products像你期望的那样; 它将保持与for_tenant先前方法改变的相同.

为了避免这种歧义并保持代码清洁,您可以使用另一种策略:

1)定义一个模块,该模块包含租户分区的所有工作逻辑:

# app/models/concerns/partitionable.rb

module Partitionable
  def self.included(base)
    base.class_eval do
      def self.tenant_model(tid)
        partition_suffix = "_#{tid}"

        table = "#{table_name}#{partition_suffix}"

        exists = connection.select_one("SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = '#{table}')")
        unless exists['exists'] == 't' || exists['exists'] == true  # different versions of pg gem give different answers
          return self # returning original model class
        end

        class_name = "#{name}#{partition_suffix}"

        model_class = Class.new(self)

        model_class.define_singleton_method(:table_name) do
          table
        end

        model_class.define_singleton_method(:name) do
          class_name
        end

        model_class
      end
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

2)在模型类中包含此模块:

class Product < PostgresDatabase
  include Partitionable

  ...
end
Run Code Online (Sandbox Code Playgroud)

3)以与预期相同的方式使用它:

Product.tenant_model(TENANT_ID).where(name: "My product")...
Run Code Online (Sandbox Code Playgroud)

那里发生了什么:

Method tenant_model(TENANT_ID)为具有ID的租户创建另一个模型类TENANT_ID.此类具有名称Product_<TENANT_ID>,与表一起使用products_<TENANT_ID>并继承Product类的所有方法.所以它可以像普通模型一样使用.阶级Product本身仍未被触及:它table_name仍然存在products.