The method
def self.for_tenant(tid)
self.table_name = "products_" + tid.to_s
self
end
makes sense, however, it has a side effect: it changes table name for Product
class. When this class is used later in the same request, for example, in this way:
Product.where(name: "My other product") ...
the table name won't be products
as you may expect; it will stay the same as changed by for_tenant
method previously.
To avoid this ambiguity and keep code clean you can use another strategy:
1) Define a module which holds all logic of work with tenant partitions:
# 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) Include this module in your model class:
class Product < PostgresDatabase
include Partitionable
...
end
3) Use it the same way as you intended:
Product.tenant_model(TENANT_ID).where(name: "My product")...
What's happened there:
Method tenant_model(TENANT_ID)
creates another model class for the tenant with ID TENANT_ID
. This class has name Product_<TENANT_ID>
, works with table products_<TENANT_ID>
and inherits all methods of Product
class. So it could be used like a regular model. And class Product
itself remains untouched: its table_name
is still products
.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…