Single Table Inheritance to simplify your code in Rails

What is the Single Table Inheritance (STI) ?

It is the idea of using a single table on several models that inherit a basic model. From the database schema, the submodels are linked by a single “type” of record (column). This is useful if you have two tables with identical fields or if you want to have one table for each template that represents an event type.

In our example, instead of having the same table for a type of user (VIP, administrator, standard, etc…), we have a contact table that is the mirror for each user class friend, working_knowledge.

We will be able to create users who can define themselves as vip and/or administrator and/or as standard. Once defined, they will be able to add their friend contacts and/or work acquaintances. It’s just for the example!

The advantage of this technique is that you can make DRY code (Don’t Repeat Yourself) instead of having 2 identical tables, with 2 models and 2 controllers. Perfection when there’s nothing left to cut. Fewer codes, more readable, maintainable, less bugs… An ecological approach.

Single Table Inheritance: database schema

Here is an overview of what we can get !

Single Table Inheritance: overview

Single Table Inheritance: Let’s go

$ rails new sti -d postgresql 
$ cd sti rails generate model User vip:integer administrator:integer standard:integer user:references 
$ git init 
$ git add . 
$ git commit -m "First commit" 
$ hub create 
$ git push origin master 
$ rails db:create && rails db:migrate

Create the Contact model and then migrate

$ rails generate model Contact first_name:string last_name:string phone_number:string birthday:date type:string:index user:references
$ rails db:migrate

Add has_many to mean that the Contact table can have multiple friends and work acquaintances

# app/models/contact.rb 
class User < ApplicationRecord   
 has_many :fiends, class_name: 'Friend'   
 has_many :working_knowledges, class_name: 'Working_knowledge' 
end

From your contact template, add the “scopes” for the templates we will create Friend and Working_knowledge

# app/models/contact.rb 
class Contact < ApplicationRecord   
 scope :friends, -> { where(type: 'Friend') } # Contact.friend
 scope :working_knowledges, -> { where(type: 'Working_knowledge') }  end

Create the Friend templates that will inherit the Contact class

# app/models/friend.rb 
 class Friend < Contact   
 belongs_to :user
 validates :first_name, :last_name, :phone_number, presence: true 
end

Create the Working_knowledge model that will inherit the Contact class

# app/models/working_knowledge.rb 
 class Working_knowledge < Contact
 belongs_to :user
 validates :first_name, :last_name, :phone_number, presence: true
end

Open routes.rb in config to add routing

# app/config/routes.rb 
Rails.application.routes.draw do   
resources :users do
 resources :friends, controller: :contacts, type: 'Friend'
 resources :working_knowledges, controller: :contacts, type: 'Working_knowledge'
end   
 root to: 'users#index'
end

To summarize here is an overview of the code for the user show view

# app/views/contacts/show.html.erb 
# [...]
<h1>Friend 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>
   <% end %>
  </tr>
 </tbody>
</table>

<h1>Working_knowledge Contacts</h1>
<%= link_to 'New', new_user_working_knowledge_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.working_knowledges.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>

The partial for the form at User

# app/views/users/_form.html.erb 
<%= form_with(model: user, local: true) do |form| %> 
# [...] 
<%end%>

Le controller Contact

# app/controllers/contact_controller.rb
class ContactsController < ApplicationController
before_action :set_contact, only: [:show, :edit, :update, :destroy] def new
 @user = User.find(params[:user_id])     
 # @contact = @user.emergencies.new
 # @contact = @user.friends.new
 # @contact = @user.send(params[:type]).new
 # DANGEROUS SEND     @contact = @user.send(set_type.pluralize).new end
   
def edit
end
   
def create
 @user = User.find(params[:user_id])
 # @contact = @user.emergencies.new(contact_params)
 # @contact = @user.friends.new(contact_params)
 @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.emergencies.find(params[:id])     
 # @contact = @user.friends.find(params[:id])
 @contact = @user.send(set_type.pluralize).find(params[:id])     
end     

def set_type       
case params[:type]       
 when 'Friend'         
  'friend'       
 when 'Working_knowledge'         
  'working_knowledge'       
 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

To access the sources, click HERE

Source

“Single Table Inheritance” Driftingruby accessed on April 01, 2018 : https://www.driftingruby.com/episodes/single-table-inheritance

signature Pierre-Christophe

No Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.