Single Table Inheritance pour simplifier votre code dans Ruby on Rails

Qu’est ce que c’est le Single Table Inheritance (STI) ?

C’est l’idée d’utiliser une seule table sur plusieurs modèles qui héritent d’un modèle de base. Depuis le schéma de la base de données, les sous-modèles sont liés par un seul “type” d’enregistrement (column). C’est utile si vous avez deux tables avec des champs identiques ou si vous voulez avoir une table pour chaque modèle qui représente un type d’événement.

Dans notre exemple, au lieu d’avoir la même table pour un type d’utilisateur (VIP, administrateur, standard, etc…), nous avons une table contact qui est le miroir pour chaque class d’utilisateur friend, working_knowledge.

Nous allons pouvoir créer des utilisateurs qui pourront se définir en tant que vip et/ou administrateur et/ou en standard. Une fois définis, ils pourront ajouter leurs contacts amis et/ou connaissance de travail. C’est juste pour l’exemple !

L’avantage de cette technique est de vous permettre de faire du code DRY (Don’t Repeat Yourself) au lieu d’avoir 2 tables identiques, avec 2 modèles et 2 contrôleurs. La perfection quand il n’y a plus rien à retrancher. Moins de codes, plus lisible, maintenable, moins de bug… Une démarche écologique.

Single Table Inheritance : schéma base de données

Voici un aperçu de ce que nous pouvons obtenir !

Single Table Inheritance : aperçu

Single Table Inheritance : C’est parti

$ 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

Créez le modèle Contact puis effectuez la migration

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

Ajoutez les has_many pour signifier que la table Contact peut avoirs plusieurs amis et connaissances de travail

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

Depuis votre modèle contact, ajoutez les “scopes” pour les modèles que nous allons créer Friend et Working_knowledge

Pour plus de détail sur le scoping

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

Créez les modèle Friend qui héritera de la class Contact

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

Créez le modèle Working_knowledge qui héritera de la class Contact

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

Ouvrez routes.rb dans config pour y ajouter le 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

Pour résumer voici un aperçu du code pour la vue show de user

# 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>

La partial pour le formulaire chez 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

Pour accéder aux sources, cliquez ICI

Source

“Single Table Inheritance” Driftingruby consulté le 01 avril 2018 : https://www.driftingruby.com/episodes/single-table-inheritance

signature Pierre-Christophe

Aucun commentaires

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.