我在Apache + mod_passenger上有一个胖的多租户Rails应用程序,该应用程序从PostgreSQL表中输出产品价格,如下所示:
Table "public.products"
Column | Type
id | bigint
name | character varying(100)
price | numeric(8,2)
然后在products.rb中,我有...
class Product < PostgresDatabase
self.table_name = "products"
# ... yadda yadda
end
我想要的是以一种非常特定的方式对“产品”表进行分区,这样我最终得到了每个租户类似products_TENANT-ID的东西(基本上是主产品表的视图,但这是另一个故事),并且能够这样的查询:
Products.for_tenant(TENANT-ID).where(:name => "My product")......
我认为我可以创建一个方法:
class Product < PostgresDatabase
self.table_name = "products"
# ... yadda yadda
def for_tenant(tid)
self.table_name = "products_" + tid.to_s
self
end
end
但是考虑到大量的流量(每秒数千个请求),这会对应用程序产生什么样的影响?我有什么想念的吗?我应该尝试其他策略吗?
非常感谢您的任何反馈/想法!
答案 0 :(得分:5)
方法
def self.for_tenant(tid)
self.table_name = "products_" + tid.to_s
self
end
是有意义的,但是它有一个副作用:它更改Product
类的表名。例如,以后在同一请求中使用此类时,例如:
Product.where(name: "My other product") ...
表名将不会像您期望的那样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
2)在您的模型类中包含此模块:
class Product < PostgresDatabase
include Partitionable
...
end
3)按预期方式使用它:
Product.tenant_model(TENANT_ID).where(name: "My product")...
发生了什么事
方法tenant_model(TENANT_ID)
为ID为TENANT_ID
的租户创建另一个模型类。此类的名称为Product_<TENANT_ID>
,可与表products_<TENANT_ID>
一起使用,并继承Product
类的所有方法。因此它可以像常规模型一样使用。类Product
本身保持不变:它的table_name
仍然是products
。