Rails single table inheritance with polymorphic association

Lets say, you wanted to store contact information such as emails, phone numbers, and websites. These contact information can be associated with an employee of a business or the business itself. A business can have multiple emails and multiple phone numbers. Similarly, an employee can have multiple emails and multiple phone numbers. In Rails, we can store all these contact information in a single table using both the polymorphic association and single-table inheritance. First, lets see how we can use polymorphic association to deal with the contact information.

We have the following active record models for a “Business” and an “Employee”:

class Business < ActiveRecord::Base; end
class Employee < ActiveRecord::Base; end

For the contact information, we have the following active record models:

 

class Email < ActiveRecord::Base; end
class PhoneNumber < ActiveRecord::Base; end
class Website < ActiveRecord::Base; end

Now, for the associations, Business and Employee models can have many emails, phone numbers and websites. And conversely, Email/Phone Number/Website can belong to either a Business model or an Employee model. How do we make up the association?

Rails ActiveRecord provides an association called “Polymorphic” association. Using polymorphic association, a model can belong to more than one other model, on a single association. Here is how this can be implemented:

class Business < ActiveRecord::Base
has_many :emails, :as => :emailable
has_many :phone_numbers, :as => phonable
has_many :websites, :as => webable
end

class Employee < ActiveRecord::Base
has_many :emails, :as => :emailable
has_many :phone_numbers, :as => phonable
has_many :websites, :as => webable
end

class Email < ActiveRecord::Base
belongs_to :emailable, :polymorphic => true
end
class PhoneNumber < ActiveRecord::Base
belongs_to :phonable, :polymorphic => true
end
class Website < ActiveRecord::Base
belongs_to :webable, :polymorphic => true
end

Having the above setup, you can get a collection of emails for a business instance via the call “@business.emails”. Similarly, you can get a collection of emails for an employee instance via “@employee.emails”.

Now, if you really think about what a contact information is? It basically contains a label and an information. I.e. Personal Email: te**@gm***.com , Business Phone Number: 1-800-999-9999, Business Website: www.google.com, etc.

So, we can say that an email, a phone number, or a website is an instance of a contact information class with two attributes: a label and an information. Thus, we can store any type of contact information in a single table.

In rails, if you name a column “type”, by default ActiveRecord allows inheritance by storing the name of the class in the column type. You can overwrite the Base.inheritance_column to change the name of the column where the class name is stored. Now, changing our polymorphic associations as “contactable”, we can have the following setup:

class Business < ActiveRecord::Base
has_many :emails, :as => :contactable, :class_name => "Email"
has_many :phone_numbers, :as => :contactable, :class_name => "PhoneNumber"
has_many :websites, :as => :contactable, :class_name => "Website"
end
class Employee < ActiveRecord::Base
has_many :emails, :as => :contactable, :class_name => "Email"
has_many :phone_numbers, :as => :contactable, :class_name => "PhoneNumber"
has_many :websites, :as => :contactable, :class_name => "Website"
end

class ContactInformation < ActiveRecord::Base
belongs_to :contactable, :polymorphic => true
end
class Email < ContactInformation
# any validation for email can go here
end
class PhoneNumber < ContactInformation
# any validation for phone numbers can go here
end
class Website < ContactInformation
# any validation for website urls can go here
end

All the emails, phone numbers and websites will now be stored in the table associated with the model “ContactInformation” and will have the following setup:

create_table :contact_informations, :force => true do |t|
t.string "contactable_type"
t.integer "contactable_id"
t.string "type"
t.string "label"
t.string "info"
t.timestamps
end

Now, when you create a business email:

@email = @business.build.emails(:label => "Personal", :info => " te**@gm***.com ");
@email.save!

the record will be saved in the table “contact_informations” with the values “Business” for the “contactable_type”, @business.id for the “contactable_id”, and “Email” for the column “type”.

Similarly, for employee phone number:

@phone = @employee.build.phone_numbers(:label => "Work", :info => "1-800-123-4567");
@phone.save!

the record will be saved in the table “contact_informations” with the values “Employee” for the “contactable_type”, @employee.id for the “contactable_id”, and “PhoneNumber” for the column “type”.

Rails makes it really easy to do a single table inheritance with polymorphic association.

This entry was posted in Uncategorized. Bookmark the permalink.

13 Responses to Rails single table inheritance with polymorphic association

  1. Thanks, that helped a lot. I’ve got into Rails at a time where polymorphic associations were a sure way to break everything. But now, I feel comfortable using them more often.

  2. Mike says:

    Thanks,,,,good work…please keep updating…

  3. Shubham Gupta says:

    Nice explanation. Thanks for the post.

  4. Tom says:

    Just wanted to say thanks, this was perfect. I was getting an error for my polymorphic STI “addresses” about missing the addressable_id – well, I hadn’t created the table properly! Thanks for writing this out!

  5. Steve says:

    Well written article. Thanks.
    One comment:
    @email = @business.build.emails(:label => "Personal", :info => "

    te**@gm***.com











    ")

    I think it should be:
    @email = @business.emails.build(:label => "Personal", :info => "

    te**@gm***.com











    ")

    Otherwise it will give an error : NoMethodError: undefined method `build’

  6. vignesh says:

    Hi, What is the difference between polymorphic association and single table inheritance?

  7. Tyler says:

    How would I implement counter_cache into this? Or will I have to manually keep up with it on update, and just select form that column for the count?

    • Tyler says:

      Let me explain a little more.

      I have an assets class

      class Asset < ActiveRecord::Base
      belongs_to :documentable, polymorphic: true, counter_cache: true
      end

      Then Image, Video, and Document. Ex:

      class Image < MediaFile
      end

      I have Images, Videos, and Documents for Products:

      class Product < ActiveRecord::Base
      has_many :images, { as: :documentable, class_name: 'Image' }
      has_many :videos, { as: :documentable, class_name: 'Video' }
      has_many :documents, { as: :documentable, class_name: 'Document' }
      end

      • Tyler says:

        I just ended up doing this. Seems to work well.

        before_save do |product|
        product.images_count = product.images.size
        product.videos_count = product.videos.size
        product.documents_count = product.documents.size
        end

  8. Rajkumar Vadla says:

    Thank you.. I helped me alot.

  9. stan says:

    Not very universal solution. It fits only in case we don’t care about different formats of data. F.e. we want to store phone as phone type – and not string. Thus, we could use nice Rails validations and so on. I think it’s not good idea to place together such a different data and store them as string together.

  10. Cheryl says:

    It’s hard to find your website in google. I found it on 22 spot, you should build quality backlinks , it will help you to get more visitors.
    I know how to help you, just type in google – k2 seo tips and tricks

  11. Natasha says:

    I read a lot of interesting articles here.
    Probably you spend a lot of time writing, i know how to save you a lot of time,
    there is an online tool that creates unique, SEO friendly articles in minutes, just type in google – laranitas free content source

Leave a Reply

Your email address will not be published. Required fields are marked *