Multiple Table Inheritance with Active Record

The situation we're trying to solve is we have a model object that requires similar behavior but different attributes. Now, think of single table inheritance and polymorphism. Single Table Inheritance ---STI, is where we'd have a base class and a couple subclasses inheriting from the base class, we use this technique when we have the same set of attributes but different behavior per subclass. This allows us keep our active record models leaner. Common scenarios you'd employ this would be a user mode with different roles, well essentially any real world object where we have commonality needing specialization. Polymorphism is where we'd have similar set of attributes and similar behavior, so it essentially allows us to associate a common domain object across multiple tables, real world examples would be tags, images, addresses.

Our problem is we have a common domain object but its specialization requires different traits to be kept but our specializations would still have mostly common behavior. This is called Multiple Table Inheritance. There are different techniques to solve this problem:

  • Citier gem, best out there but not really maintained and the solution utilizes database view. Essentially, excessively complicated in my opinion.
  • Using dynamic attributes and allowing a user to determine what attributes are created at runtime. This is a common technique I use and is very flexible. But it doesn't exactly fit our situation.
  • Use polymorphism, most of the gems aiming to solve this problem use it, but I don't like creating concrete tables for abstract objects.
  • Using STI and storing those different attributes for each subclass in a hash on the base class.
  • Not doing, modeling your domain object another way.

I want to show you guys number four and how I implemented it in a 3.2 project.

Make sure the Postgres Hstore extension is enabled in our DB, run this simple migration

class SetupHstore < ActiveRecord::Migration
  def self.up
    execute "CREATE EXTENSION IF NOT EXISTS hstore"
  end

  def self.down
    execute "DROP EXTENSION IF EXISTS hstore"
  end
end

Add the Postgres HStore datatype to our Active Record subclasses (Rails 4.0 adds native HStore support so the specify the Coder will become unnecessary nor would you need to use the above migration.

app/models/user.rb

class User < ActiveRecord:Base
  serialize :settings, ActiveRecord::Coders::Hstore
end

Rails uses inflection to determine the class type at runtime for route generation so we can user newuserpath for admins, clients, or designers. Add the below code to your base User class:

app/models/user.rb

# This assures that the child object is routed through the parent object
def self.inherited(child)
  child.instance_eval do
    alias :original_model_name :model_name
    def model_name
      User.model_name
    end
  end
  super
end

app/models/admin.rb

class Admin < User
  %w[auth_token account_number].each do |key|
    attr_accessible key
    define_method(key) do
      settings && settings[key]
  end

  define_method("#{key}=") do |value|
    self.settings = (settings || {}).merge(key => value)
  end
 end
...

app/models/client.rb

class Client < User
%w(username password auth_token).each do |key|
  attr_accessible key
  define_method(key) do
    settings && settings[key]
  end

  define_method("#{key}=") do |value|
      self.settings = (settings || {}).merge(key => value)
  end
 end
...

Setup up our controller instances for our forms, use a private controller method

app/controllers/user_controller.rb

def new
  setup_user_model
  #…..
end

def create
  setup_user_model
  #….
end

private

    def setup_user_model
        model = nil
        if !params[:user].blank? and !params[:user]   [:type].blank?
        model = params[:user].delete(:type).constantize.to_s
        end
        model.nil? 
        ? @user = User.new(params[:user]) 
        : @user = model.constantize.new(params[:user])
      @user.type = model
      end

Now, we can include the above code in each of our subclasses as is and we would be good, but lets adhere to a proper DRY. Lets create an Active Record extension and extend our Active Record extensions with a convenient macro. I prefer using a concern as this is the encouraged way to extend ActiveRecord, of course you can also monkey patch etc..

app/models/admin.rb

dynamic_user :username, :password

*app/models/client.rb * dynamicuser :email, :authtoken, :password

app/lib/dynamic.rb

module ActiveRecordDynamicUser

  extend ActiveSupport::Concern

    module ClassMethods
    def dynamic_user(*opts)
      opts.map {|x| x.to_s}.each do |key|
        attr_accessible key
        define_method(key) do
          settings && settings[key]
        end

        define_method("#{key}=") do |value|
          self.settings = (settings || {}).merge(key => value)
        end
        end
    end
  end
end

ActiveRecord::Base.send(:include, ActiveRecordDynamicUser)

There you have it, with out method_missing code we gain access to our hstore hash and can access them from rails forms. Using this technique requires no special code in the views. So you're forms won't need the #becomes hack or you won't have a long list of custom routes to solve the record identification problem.

Additionally this method essentially negates the first caveat in using STI where you're master table can get filled with null's in all those columns being used by the subclasses.

Testing:

Testing this solution would be no different from a normal STI situation. . I like RSpec and you would normally test STI with shared examples to test the parent behavior with smaller tests for each subclass.

I'm turning on comments in hopes of starting a dialogue on this technique. Of course, I do not claim discovering this for this is just an amalgamation of existing solutions but I wanted to get my thoughts out there in hopes of making this available to others stumbling for an answer to a similar problem.