Changing table name at query run time in a Rails application

后端 未结 1 945
一个人的身影
一个人的身影 2020-12-10 23:37

I have a fat multi-tenant Rails app on Apache + mod_passenger that outputs product prices from a PostgreSQL table as follows:

Table \"public.products\"
Colum         


        
1条回答
  •  难免孤独
    2020-12-11 00:00

    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_, works with table products_ 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.

    0 讨论(0)
提交回复
热议问题