Mengenal Single Table Inheritance pada Rails (Contoh 1)
Prerequisite
ruby 2.6.3 rails 5.2.4 postgresql 11.5
Prakata
Apa itu Single Table Inheritance?
Dapat didefinisikan sebagai tabel induk yang mewariskan sifat-sifatnya pada tabel anakan yang berelasi dengannya.
Ahahaha (^_^) definisi macam apa itu.
Abaikan.
Pada saat mengimplementasikan Single Table Inheritance (STI), saya menemukan lebih dari satu cara pada Rails. Maka dari itu, tulisan ini akan saya bagi dalam beberapa contoh.
Catatan kali ini adalah contoh pertama.
Kira-kira seperti ini ERD-nya.
Gambar 1. ERD Single Table Inheritance contacts dengan friends dan emergencies
Migrations
Saya membuat dua buah model migration untuk tabel users dan contacts.
users
$ rails g model user email first_name last_name
1class CreateUsers < ActiveRecord::Migration[5.2]2 def change3 create_table :users do |t|4 t.string :email5 t.string :first_name6 t.string :last_name7 t.timestamps8 end9 end10endcontacts
$ rails g model contact user_id:integer type first_name last_name phone_number
1class CreateContacts < ActiveRecord::Migration[5.2]2 def change3 create_table :contacts do |t|4 t.integer :user_id5 t.string :type6 t.string :first_name7 t.string :last_name8 t.string :phone_number9 t.timestamps10 end11 add_index :contacts, [:type, :user_id]12 end13endBagian penting yang harus ditambahkan adalah,
add_index :contacts, [:type, :user_id]
Kemudian jalankan migration tersebut.
$ rails db:migrate
Models
Setelah migration berhasil dijalankan, saya akan membuat scope pada model contact untuk model friend dan emergency.
1class Contact < ApplicationRecord2 scope :friends, -> { where(type: 'Friend') } # Contact.friends3 scope :emergency, -> { where(type: 'Emergency') } # Contact.emergencies4endNah, kemudian tinggal buat kedua model tersebut.
1class Friend < Contact2 belongs_to :user3end1class Emergency < Contact2 belongs_to :user3endSelanjutnya, model user yang memiliki relation has_many dengan kedua model tersebut.
1class User < ApplicationRecord2 has_many :friends, class_name: 'Friend'3 has_many :emergencies, class_name: 'Emergency'4endControllers
Model sudah jadi, selanjutnya mengatur controller.
Saya akan mulai dari users controller yang tidak perlu ada modifikasi.
1class UsersController < ApplicationController2 before_action :set_user, only: [:show, :edit, :update, :destroy]34 # GET /users5 def index6 @users = User.all7 end89 # GET /users/110 def show; end1112 # GET /users/new13 def new14 @user = User.new15 end1617 # POST /users18 def create19 @user = User.new(user_params)2021 if @user.save22 redirect_to @user, notice: 'User was successfully created.'23 else24 render :new25 end26 end2728 # GET /users/1/edit29 def edit; end3031 # PATCH/PUT /users/132 def update33 if @user.update(user_params)34 redirect_to @user, notice: 'User was successfully updated.'35 else36 render :edit37 end38 end3940 # DELETE /users/141 def destroy42 @user.destroy43 redirect_to users_url, notice: 'User was successfully destroyed.'44 end4546 private4748 # Use callbacks to share common setup or constraints between actions.49 def set_user50 @user = User.find(params[:id])51 end5253 # Only allow a list of trusted parameters through.54 def user_params55 params.require(:user).permit(:first_name, :last_name, :email)56 end57endNah, selanjutnya contacts controller yang akan menggunakan object user di dalamnya.
1class ContactsController < ApplicationController2 before_action :set_contact, only: [:edit, :update, :destroy]34 # GET /contacts/new5 def new6 @user = User.find(params[:user_id])7 @contact = @user.send(set_type.pluralize).new8 end910 # POST /contacts11 def create12 @user = User.find(params[:user_id])13 @contact = @user.send(set_type.pluralize).new(contact_params)1415 if @contact.save16 redirect_to @user, notice: 'Contact was successfully created.'17 else18 render :new19 end20 end2122 # GET /contacts/1/edit23 def edit; end2425 # PATCH/PUT /contacts/126 def update27 if @contact.update(contact_params)28 redirect_to @user, notice: 'Contact was successfully updated.'29 else30 render :edit31 end32 end3334 # DELETE /contacts/135 def destroy36 @contact.destroy37 redirect_to @user, notice: 'Contact was successfully destroyed.'38 end3940 private4142 # Use callbacks to share common setup or constraints between actions.43 def set_contact44 @user = User.find(params[:user_id])45 @contact = @user.send(set_type.pluralize).find(params[:id])46 end4748 def set_type49 case params[:type]50 when 'Friend'51 'friend'52 when 'Emergency'53 'emergency'54 end55 end5657 # Only allow a list of trusted parameters through.58 def contact_params59 params.require(set_type.to_sym).permit(60 :user_id, :type, :first_name, :last_name, :phone_number, :address,61 :city, :state, :birthday62 )63 end64endRoutes
Pada routes, saya akan menggunakan namespace untuk :users.
1Rails.application.routes.draw do2 root to: 'users#index'34 resources :users do5 resources :friends, controller: :contacts, type: 'Friend'6 resources :emergencies, controller: :contacts, type: 'Emergency'7 end8endDari routes tersebut, saya akan mendapatkan route seperti ini.
root GET / users#index
user_friends GET /users/:user_id/friends(.:format) contacts#index {:type=>"Friend"}
POST /users/:user_id/friends(.:format) contacts#create {:type=>"Friend"}
new_user_friend GET /users/:user_id/friends/new(.:format) contacts#new {:type=>"Friend"}
edit_user_friend GET /users/:user_id/friends/:id/edit(.:format) contacts#edit {:type=>"Friend"}
user_friend GET /users/:user_id/friends/:id(.:format) contacts#show {:type=>"Friend"}
PATCH /users/:user_id/friends/:id(.:format) contacts#update {:type=>"Friend"}
PUT /users/:user_id/friends/:id(.:format) contacts#update {:type=>"Friend"}
DELETE /users/:user_id/friends/:id(.:format) contacts#destroy {:type=>"Friend"}
user_emergencies GET /users/:user_id/emergencies(.:format) contacts#index {:type=>"Emergency"}
POST /users/:user_id/emergencies(.:format) contacts#create {:type=>"Emergency"}
new_user_emergency GET /users/:user_id/emergencies/new(.:format) contacts#new {:type=>"Emergency"}
edit_user_emergency GET /users/:user_id/emergencies/:id/edit(.:format) contacts#edit {:type=>"Emergency"}
user_emergency GET /users/:user_id/emergencies/:id(.:format) contacts#show {:type=>"Emergency"}
PATCH /users/:user_id/emergencies/:id(.:format) contacts#update {:type=>"Emergency"}
PUT /users/:user_id/emergencies/:id(.:format) contacts#update {:type=>"Emergency"}
DELETE /users/:user_id/emergencies/:id(.:format) contacts#destroy {:type=>"Emergency"}
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
Views
Selanjutnya view template.
+ ...
- views/
- contacts/
_form.html.erb
edit.html.erb
new.html.erb
+ layouts/
- users/
- show/
_table_body.html.erb
_form.html.erb
edit.html.erb
index.html.erb
new.html.erb
show.html.erb
+ ...
Yang terpenting adalah users shows.
1...2...34<h1>Emergency Contacts</h1>5<%= link_to '+ New', new_user_emergency_path(@user) %>6<table>7 <thead>8 <tr>9 <th>First Name</th>10 <th>Last Name</th>11 <th>Phone Number</th>12 <th>Birthday</th>13 <th>Action</th>14 </tr>15 </thead>16 <tbody>17 <% @user.emergencies.each do |contact| %>18 <%= render 'users/show/table_body', user: @user, contact: contact %>19 <% end %>20 </tbody>21</table>2223<h1>Friends Contacts</h1>24<%= link_to '+ New', new_user_friend_path(@user) %>25<table>26 <thead>27 <tr>28 <th>First Name</th>29 <th>Last Name</th>30 <th>Phone Number</th>31 <th>Birthday</th>32 <th>Action</th>33 </tr>34 </thead>35 <tbody>36 <% @user.friends.each do |contact| %>37 <%= render 'users/show/table_body', user: @user, contact: contact %>38 <% end %>39 </tbody>40</table>Partial dari users/show/_table_body.
1<tr>2 <td><%= contact.first_name %></td>3 <td><%= contact.last_name %></td>4 <td><%= contact.phone_number %></td>5 <td><%= contact.birthday %></td>6 <td>7 <%= link_to 'Edit', edit_user_emergency_path(user, contact) %> |8 <%= link_to 'Delete', [user, contact], method: :delete %>9 </td>10</tr>Lalu form dari contacts/_form.
1<%= form_with(model: [user, contact], local: true) do |form| %>2 ...3 ...4<% end %>Yang perlu diperhatikan adalah pada bagian kedua partial di atas.
Terdapat [user, contact], karena contact merupakan controller namespace dan routing.
Oke, sepertinya segini aja.
Apaila teman-teman ingin melihat detail projectnya lebih jelas, ada di repository GitHub milik saya, di sini.
Mudah-mudahan dapat bermanfaat buat teman-teman.
Terima kasih.
(^_^)
Referensi
-
www.driftingruby.com/episodes/single-table-inheritance
Diakses tanggal: 2020/02/21 -
guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
Diakses tanggal: 2020/02/21 -
api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
Diakses tanggal: 2020/02/21