Introduction

Polymorphic associations in Ruby on Rails allow you to have a model associated with more than one other model without the use of a join table. This works by adding type and id field to the table of the model you wish to make polymorphic. For example, lets say we have 3 models. The first 2 models, Business and Person, each can have interactions associated with them. These interactions share the same business logic regardless of the associated model so we can easily make interactions polymorphic.

Rails Application Setup

In order to utilize polymorphic associations in this tutorial, we will first need to create a few models. The first model, Person, will store information on people. The second model, business, will store business information. The third model, Interaction, will store simple descriptions of various phone calls and other interactions that the user has had. Lets generate these models now. Run the commands listed below to create these models.

Terminal Commands:

rails g model Person first_name last_name interaction:references{polymorphic}
rails g model Business name
rails g model Interaction description

rake db:migrate

Now that we have created these models, lets add some code to these models. First, open up your Person model and modify it so that it looks like the following code.

app/models/person.rb:

class Person < ActiveRecord::Base
  has_many :interactions, as: :interactive
end

We also want to add the same code to our business model. Open up your business model and modify it so that it looks like the following code.

app/models/business.rb:

class Business < ActiveRecord::Base
  has_many :interactions, as: :interactive
end

Great! Now open up the interaction model and verify that it looks like the following code. Rails should have already inserted the necessary line, but it doesn't hurt to double check.

app/models/business.rb:

class Interaction < ActiveRecord::Base
  belongs_to :interactive, polymorphic: true
end

Now if we open up a rails console using the rails console command and type Interaction.column_names, we will notice that there are two columns, interactive_id, and interactive_type. The interactive_type column holds the type of the model that the interaction belongs to. The interactive_id holds the id of the model that the interaction belongs to. Together these two columns tell rails what model this interaction should be associated with.

Now that we are finished with the models, it's time to create the controllers. We will create three controllers. The first controller, People, will allow us to create, edit, and update people. The second controller, Businesses, will allow us to create, edit, and update businesses. The final controller, Interactions, will handle the saving of each interaction. Run the commands below to create these controllers now.

Terminal Commands:

rails g controller People index show new create edit update
rails g controller Businesses index show new create edit update
rails g controller Interactions new create edit update

Now we need to add a few routes. Open up your routes file and add the following code, making sure not to overwrite your own application name with the example application name listed below.

config/routes.rb:

PolymorphicExample::Application.routes.draw do
  resources :people do 
    resources :interactions, only: [:new, :create, :edit, :update]
  end

  resources :businesses do
    resources :interactions, only: [:new, :create, :edit, :update]
  end

  root to: "people#index"
end

These routes set up both people and businesses for editing, and also adds a nested resource for interactions. This allows us to easily create interactions for both people and businesses.

Now lets add some code to each controller to handle reads, writes, and deletions. First open your people controller and modify it so that it looks like the code listed below.

app/controllers/people_controller.rb:

class PeopleController < ApplicationController
  def index
    @people = Person.all
  end

  def show
    @person = Person.find(params[:id])
  end

  def new
    @person = Person.new
  end

  def create
    @person = Person.new(person_params)

    if @person.save
      redirect_to people_path, notice: "The person has been successfully created."
    end
  end

  def edit
    @person = Person.find(params[:id])
  end

  def update
    @person = Person.find(params[:id])
    if @person.update_attributes(person_params)
      redirect_to people_path, notice: "The person has been updated"
    end
  end

  def destroy
    @person = Person.find(params[:id])
    @person.destroy
  end
private
  def person_params
    params.require(:person).permit!
  end
end

Next, lets modify the businesses controller. You'll notice they look pretty similar.

app/controllers/businesses_controller.rb:

class BusinessesController < ApplicationController
  def index
    @businesses = Business.all
  end

  def show
    @business = Business.find(params[:id])
  end

  def new
    @business = Business.new
  end

  def create
    @business = Business.new(business_params)

    if @business.save
      redirect_to businesses_path, notice: "The business has been successfully created."
    end
  end

  def edit
    @business = Business.find(params[:id])
  end

  def update
    @business = Business.find(params[:id])
    if @business.update_attributes(business_params)
      redirect_to businesses_path, notice: "The business has been updated"
    end
  end

  def destroy
    @business = Business.find(params[:id])
    @business.destroy
  end
private
  def business_params
    params.require(:business).permit!
  end
end

Great, now lets add some code for the interactions controller. Open up the interactions controller and modify it so that it looks like the code listed below.

app/controllers/interactions_controller.rb:

class InteractionsController < ApplicationController
  def new
    @context = context
    @interaction = @context.interactions.new
  end

  def create
    @context = context
    @interaction = @context.interactions.new(interaction_params)

    if @interaction.save
      redirect_to context_url(context), notice: "The interaction has been successfully created."
    end
  end

  def edit
    @context = context
    @interaction = context.interactions.find(params[:id])
  end

  def update
    @context = context
    @interaction = @context.interactions.find(params[:id])
    if @interaction.update_attributes(interaction_params)
      redirect_to context_url(context), notice: "The interaction has been updated"
    end
  end

private
  def interaction_params
    params.require(:interaction).permit!
  end

  def context
    if params[:person_id]
      id = params[:person_id]
      Person.find(params[:person_id])
    else
      id = params[:business_id]
      Business.find(params[:business_id])
    end
  end 

  def context_url(context)
    if Person === context
      person_path(context)
    else
      business_path(context)
    end
  end
end

Now lets add some code for the views. First, lets modify the layout a bit so that we can include Bootstrap. By linking to the bootstrap files on the Yandex CDN we can quickly and easily build testing apps such as this one. Modify your application layout so that i tlooks like the code listed below.

app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
<head>
  <title>PolymorphicExample</title>
  <%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
  <%= stylesheet_link_tag "http://yandex.st/bootstrap/3.0.2/css/bootstrap.min.css", media: "all" %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "http://yandex.st/bootstrap/3.0.2/js/bootstrap.min.js" %>
  <%= csrf_meta_tags %>
</head>
<body>
  <nav class="navbar navbar-inverse navbar-static-top">
    <div class="container">
      <ul class="nav navbar-nav">
        <li><%= link_to "People", people_path %></li>
        <li><%= link_to "Businesses", businesses_path %></li>
      </ul>
    </div>
  </nav>
   
  <div class="container">
    <%= yield %>
  </div>

</body>
</html>

Now lets create the views for businesses. First we need to create a partial for the edit form. Lets do this now. Create _form.html.erb for businesses and add in the code listed below.

app/views/businesses/_form.html.erb:

<%= form_for @business do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: "form-control", placeholder: "Enter the name of the business" %>
  </div>
  <%= f.submit class: "btn btn-primary" %>
<% end %>

Next, lets add in the code for the edit view.

app/views/businesses/edit.html.erb:

<h1>Edit Business</h1>
<%= render "form" %>

Now lets add in the code for the index view.

app/views/businesses/index.html.erb:

<h1>Businesses</h1>
<div class="well">
<%= link_to "New Business", new_business_path, class: "btn btn-primary" %>
</div>
<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Number of Interactions</th>
      <th>&nbsp;</th>
    </tr>
  </thead>
  <tbody>
    <% if @businesses.count == 0 %>
      <tr>
        <td colspan="3">
          No businesses found.  Click the button above to add a new one.
        </td>
      </tr>
    <% else %>
      <% @businesses.each do |business| %>
        <tr>
          <td><%= link_to business.name, business %></td>
          <td><%= business.interactions.count %></td>
          <td><%= link_to "Edit", edit_business_path(business), class: "btn btn-default" %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>
</table>

Now for the new view.

app/views/businesses/new.html.erb:

<h1>Create a New Business</h1>
<%= render "form" %>

Finally, lets add code for the show view for businesses. Note that the show view lists interactions with the business the user has had.

app/views/businesses/show.html.erb:

<h1><%= @business.name %></h1>
<hr />
<h3>Interactions</h3>
<div class="well">
  <%= link_to "New Interaction", new_business_interaction_path(@business), class: "btn btn-primary" %>
</div>
<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>Description</th>
      <th>Created On</th>
      <th>&nbsp;</th>
    </tr>
  </thead>
  <% if @business.interactions.count > 0 %>
    <% @business.interactions.each do |interaction| %>
      <tr>
        <td><%= interaction.description %></td>
        <td><%= time_ago_in_words interaction.created_at %> ago</td>
        <td><%= link_to "Edit", edit_business_interaction_path(@business, interaction), class:"btn btn-default" %></td>
      </tr>
    <% end %>
  <% else %>
    <tr>
      <td colspan="2">No interactions found for this business.  Click the 'New Interaction' button to create a new interaction.</td>
    </tr>
  <% end %>
</table>

Phew! Now that businesses are done, we can focus on people. Note that the code is largely similar and could definitely be DRYed up, but it's kept as is for simplicity's sake. First, lets create a form view for people.

app/views/people/_form.html.erb:

<%= form_for @person do |f| %>
  <div class="form-group">
    <%= f.label :first_name %>
    <%= f.text_field :first_name, class: "form-control", placeholder: "Enter a First Name" %>
  </div>
  <div class="form-group">
    <%= f.label :last_name %>
    <%= f.text_field :last_name, class: "form-control", placeholder: "Enter a Last Name" %>
  </div>
  <%= f.submit class: "btn btn-primary" %>
<% end %>

Now lets add in the code for the edit view for people.

app/views/people/edit.html.erb:

<h1>Editing <%= @person.name %></h1>
<%= render "form" %>

Next, lets add in the code for the index view.

app/views/people/index.html.erb:

<h1>People</h1>
<div class="well">
<%= link_to "New Person", new_person_path, class: "btn btn-primary" %>
</div>
<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Number of Interactions</th>
      <th>&nbsp;</th>
    </tr>
  </thead>
  <tbody>
    <% if @people.count == 0 %>
      <tr>
        <td colspan="3">
          No people found.  Click the button above to add a new one.
        </td>
      </tr>
    <% else %>
      <% @people.each do |person| %>
        <tr>
          <td><%= link_to "#{person.first_name} #{person.last_name}", person %></td>
          <td><%= person.interactions.count %></td>
          <td><%= link_to "Edit", edit_person_path(person), class: "btn btn-default" %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>
</table>

Now lets tackle the new view for people.

app/views/people/new.html.erb:

<h1>Create a New Person</h1>
<%= render "form" %>

Finally, lets add in the show view for people.

app/views/people/show.html.erb:

<h1><%= "[email protected]_name} [email protected]_name}" %></h1>
<hr />
<h3>Interactions</h3>
<div class="well">
  <%= link_to "New Interaction", new_person_interaction_path(@person), class: "btn btn-primary" %>
</div>
<table class="table table-bordered table-striped">
  <thead>
    <tr>
      <th>Description</th>
      <th>Created On</th>
      <td>&nbsp;</td>
    </tr>
  </thead>
  <% if @person.interactions.count > 0 %>
    <% @person.interactions.each do |interaction| %>
      <tr>
        <td><%= interaction.description %></td>
        <td><%= time_ago_in_words interaction.created_at %> ago</td>
        <td><%= link_to "Edit", edit_person_interaction_path(@person, interaction), class: "btn btn-default" %></td>
      </tr>
    <% end %>
  <% else %>
    <tr>
      <td colspan="2">No interactions found for this person.  Click the 'New Interaction' button to create a new interaction.</td>
    </tr>
  <% end %>
</table>

That's quite a bit of code. We are almost there. We just have to add in a couple views for the interactions themselves. First, lets create the form view for interactions.

app/views/interactions/_form.html.erb:

<%= form_for [@context, @interaction] do |f| %>
  <div class="form-group">
    <%= f.label :description %>
    <%= f.text_area :description, class: "form-control", placeholder: "Enter a description of the interaction you had with this entity.", rows: 10 %>
  </div>

  <%= f.submit class: "btn btn-primary" %>
<% end %>

Now lets add the code for the edit view for interactions.

app/views/interactions/edit.html.erb:

<h1>Editing Interaction for <%= @context.name %></h1>
<%= render "form" %>

Finally, lets add in the code for the new view for interactions.

app/views/interactions/new.html.erb:

<h1>New Interaction for <%= @context.name %></h1>
<%= render "form" %>

With that we are finished. Now if you start a rails server and connect to http://localhost:3000 you will see that you can create both people and businesses. Upon creating a person or a business you can click on them and add interactions. That's it! Thanks for reading!