【问题标题】:Changing table name at query run time in a Rails application在 Rails 应用程序的查询运行时更改表名
【发布时间】:2018-09-27 13:18:07
【问题描述】:

我在 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

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

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

【问题讨论】:

    标签: ruby-on-rails ruby activerecord rails-activerecord


    【解决方案1】:

    方法

    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_&lt;TENANT_ID&gt;,与表products_&lt;TENANT_ID&gt; 一起使用并继承Product 类的所有方法。所以它可以像普通模型一样使用。而Product 类本身保持不变:它的table_name 仍然是products

    【讨论】:

    • 嗨伊利亚。非常好的答案,谢谢你。我不介意表名是否在同一请求中保持更改,因为同一请求来自特定租户(网站)的用户。如果 table_name 在一个请求中设置并且对于其他请求(用户)保持不变,它只会成为一个问题。无论如何,我计划重构所有控制器以使用 Products.for_tenant(TENANT-ID) 。在这种特定情况下,我是否仍然应该实现您的模块,还是可以按照我最初的想法去做?基本上,这减少了确定 self.table_name 行为与请求的确切方式的问题。
    • 你决定 :) 我的工作经验告诉我,有副作用的方法通常不是一个好主意。如果你不喜欢使用这个模块,并且很确定用户请求应该只适用于一个租户,还有另一种策略:为你的基本控制器定义一个过滤器,它将为 Product 类设置表名,并使用这个类后来没有任何额外的方法。因此,从该基本控制器继承的所有控制器都将针对该特定租户调整 Product 类。
    • 谢谢伊利亚,你太棒了!太糟糕了,不允许给人们送啤酒!
    • 希望有一天它会被实施 :) 谢谢!
    • 是的,它也可以。我以更复杂的方式编写了该检查,因为我的代码应该适用于不同版本的 Rails。
    猜你喜欢
    • 2016-02-25
    • 2012-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-22
    • 2014-01-22
    • 2014-09-03
    • 1970-01-01
    相关资源
    最近更新 更多