Jul 18, 2015

Tip: Dynamic Models in ActiveRecord with PostgreSQL

One of the most recent problems I had to face was to dynamically access tables based on a certain pattern. Think of tables with the following format: table_1, table_2table_n, or any other format to be honest, just think of a well known format.

Another interesting thing about this problem was also the fact we are required to use both PostgreSQL and Schemas to somehow organize the data in a better way. So, how to dynamically create class models that also happen to use different schemas in PostgreSQL with ActiveRecord?

The solution is pretty simple and really straightforward:

require 'active_record'

class ModelFactory < ActiveRecord::Base
  self.abstract_class = true

  class << self
    def build_model(params)
      schema = params[:schema]
      table = params[:table]
      full_name = "#{schema}.#{table}"

      create_database_objects(schema, table, full_name)

      model = Class.new(ModelFactory) do
        self.table_name = full_name
      end

      model
    end

    private
    def create_database_objects(schema, table, full_name)
      connection = ModelFactory.connection

      # Create Schema 
      unless connection.schema_exists?(schema)
        begin
          connection.create_schema(schema)
        rescue PG::DuplicateSchema
        end
      end

      # Create Table
      new_table = false
      unless connection.table_exists?(full_name)
        new_table = true
        begin
          connection.create_table(model.table_name) do |table|
            table.column :name, :string, limit: 50, null: false
            table.column :address, :string, limit: 300, null: false
          end
        rescue PG::DuplicateTable
        end
      end

      # What's next? Maybe adding indexes, who knows, sky is the limit
    end
  end

end

With that you can easily do something like:

model = ModelFactory.build_model({schema: 'important_stuff', table: 'some_table'})
model.new(name: 'Mario', address: '742 Evergreen Terrace').save!

The explanation of the code above is simple:

  1. You need an abstract class because your new dynamic classes have to subclass in order to reference the real table.
  2. The method ModelFactory.build_model is used for creating new models.
  3. Besides creating the model, the actual schema and table is created in case they do not exist, the creation is wrapped in a begin/rescue section in case multiple ModelFactory instances receive the same message for creating the same schema+table at the same time.

Clear and specially simple solution.