Single Table Inheritance

Episode #97 by Teacher's Avatar David Kimura

Summary

Learn to use single table inheritance to allow multiple classes to be stored in the same database table.
rails model 8:33

Resources

Summary

# 20170910010823_create_contacts.rb
# rails g model contact user_id:integer type first_name last_name phone_number address city state zip birthday

class CreateContacts < ActiveRecord::Migration[5.1]
  def change
    create_table :contacts do |t|
      t.integer :user_id
      t.string :type
      t.string :first_name
      t.string :last_name
      t.string :phone_number
      t.string :address
      t.string :city
      t.string :state
      t.string :zip
      t.date :birthday
      t.timestamps
    end
    add_index :contacts, [:type, :user_id]
  end
end

# contact.rb
class Contact < ApplicationRecord
  scope :friends, -> { where(type: 'Friend') }
  scope :emergencies, -> { where(type: 'Emergency') }
end

# emergency.rb
class Emergency < Contact
  belongs_to :user
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :phone_number, presence: true
end

# friend.rb
class Friend < Contact
  belongs_to :user
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :birthday, presence: true
end

# user.rb
class User < ApplicationRecord
  has_many :emergencies, class_name: 'Emergency'
  has_many :friends, class_name: 'Friend'
end

# routes.rb
Rails.application.routes.draw do
  resources :users do
    resources :emergencies, controller: :contacts, type: 'Emergency'
    resources :friends, controller: :contacts, type: 'Friend'
  end
  root to: 'users#index'
end

# show.html.erb
<h1>Emergency Contacts</h1>

<%= link_to 'New', new_user_emergency_path(@user) %>
<table class='table'>
  <thead>
    <tr>
      <th></th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Phone Number</th>
      <th>Birthday</th>
    </tr>
  </thead>
  <tbody>
    <% @user.emergencies.each do |contact| %>
      <tr>
        <td><%= link_to 'delete', [@user, contact], method: :delete %></td>
        <td><%= contact.first_name %></td>
        <td><%= contact.last_name %></td>
        <td><%= contact.phone_number %></td>
        <td><%= contact.birthday %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<h1>Friends Contacts</h1>
<%= link_to 'New', new_user_friend_path(@user) %>
<table class='table'>
  <thead>
    <tr>
      <th></th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Phone Number</th>
      <th>Birthday</th>
    </tr>
  </thead>
  <tbody>
    <% @user.friends.each do |contact| %>
      <tr>
        <td><%= link_to 'delete', [@user, contact], method: :delete %></td>
        <td><%= contact.first_name %></td>
        <td><%= contact.last_name %></td>
        <td><%= contact.phone_number %></td>
        <td><%= contact.birthday %></td>
      </tr>
    <% end %>
  </tbody>
</table>

_form.html.erb
<%= form_with model: [@user, contact], local: contact do |form| %>
  ...
<% end %>

# contacts_controller.rb
class ContactsController < ApplicationController
  before_action :set_contact, only: [:show, :edit, :update, :destroy]

  def new
    @user = User.find(params[:user_id])
    @contact = @user.send(set_type.pluralize).new
  end

  def edit
  end

  def create
    @user = User.find(params[:user_id])
    @contact = @user.send(set_type.pluralize).new(contact_params)
    if @contact.save
      redirect_to @user, notice: "#{params[:type]} Contact was successfully created."
    else
      render :new
    end
  end

  def update
    if @contact.update(contact_params)
      redirect_to @user, notice: "#{params[:type]} Contact was successfully updated."
    else
      render :edit
    end
  end

  def destroy
    @contact.destroy
    redirect_to @user, notice: "#{params[:type]} was successfully destroyed."
  end

  private

    def set_contact
      @user = User.find(params[:user_id])
      @contact = @user.send(set_type.pluralize).find(params[:id])
    end

    def set_type
      case params[:type]
      when 'Friend'
        'friend'
      when 'Emergency'
        'emergency'
      end
    end

    def contact_params
      params.require(set_type.to_sym).permit(:type, :first_name, :last_name, :phone_number, :address, :city, :state, :zip, :birthday)
    end
end