BanditHijo.dev

Mengenal Single Table Inheritance pada Rails (Contoh 1)

Created at: 2020-02-21
Author by: BanditHijo

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

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
db/migrations/20200219025008_create_users.rb
1class CreateUsers < ActiveRecord::Migration[5.2]
2 def change
3 create_table :users do |t|
4 t.string :email
5 t.string :first_name
6 t.string :last_name
7 t.timestamps
8 end
9 end
10end

contacts

$ rails g model contact user_id:integer type first_name last_name phone_number
db/migrations/20200219025125_create_contacts.rb
1class CreateContacts < ActiveRecord::Migration[5.2]
2 def change
3 create_table :contacts do |t|
4 t.integer :user_id
5 t.string :type
6 t.string :first_name
7 t.string :last_name
8 t.string :phone_number
9 t.timestamps
10 end
11 add_index :contacts, [:type, :user_id]
12 end
13end

Bagian 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.

app/models/contact.rb
1class Contact < ApplicationRecord
2 scope :friends, -> { where(type: 'Friend') } # Contact.friends
3 scope :emergency, -> { where(type: 'Emergency') } # Contact.emergencies
4end

Nah, kemudian tinggal buat kedua model tersebut.

app/models/friend.rb
1class Friend < Contact
2 belongs_to :user
3end
app/models/emergency.rb
1class Emergency < Contact
2 belongs_to :user
3end

Selanjutnya, model user yang memiliki relation has_many dengan kedua model tersebut.

app/model/user.rb
1class User < ApplicationRecord
2 has_many :friends, class_name: 'Friend'
3 has_many :emergencies, class_name: 'Emergency'
4end

Controllers

Model sudah jadi, selanjutnya mengatur controller.

Saya akan mulai dari users controller yang tidak perlu ada modifikasi.

app/controllers/users_controller.rb
1class UsersController < ApplicationController
2 before_action :set_user, only: [:show, :edit, :update, :destroy]
3
4 # GET /users
5 def index
6 @users = User.all
7 end
8
9 # GET /users/1
10 def show; end
11
12 # GET /users/new
13 def new
14 @user = User.new
15 end
16
17 # POST /users
18 def create
19 @user = User.new(user_params)
20
21 if @user.save
22 redirect_to @user, notice: 'User was successfully created.'
23 else
24 render :new
25 end
26 end
27
28 # GET /users/1/edit
29 def edit; end
30
31 # PATCH/PUT /users/1
32 def update
33 if @user.update(user_params)
34 redirect_to @user, notice: 'User was successfully updated.'
35 else
36 render :edit
37 end
38 end
39
40 # DELETE /users/1
41 def destroy
42 @user.destroy
43 redirect_to users_url, notice: 'User was successfully destroyed.'
44 end
45
46 private
47
48 # Use callbacks to share common setup or constraints between actions.
49 def set_user
50 @user = User.find(params[:id])
51 end
52
53 # Only allow a list of trusted parameters through.
54 def user_params
55 params.require(:user).permit(:first_name, :last_name, :email)
56 end
57end

Nah, selanjutnya contacts controller yang akan menggunakan object user di dalamnya.

app/controllers/contacts_controller.rb
1class ContactsController < ApplicationController
2 before_action :set_contact, only: [:edit, :update, :destroy]
3
4 # GET /contacts/new
5 def new
6 @user = User.find(params[:user_id])
7 @contact = @user.send(set_type.pluralize).new
8 end
9
10 # POST /contacts
11 def create
12 @user = User.find(params[:user_id])
13 @contact = @user.send(set_type.pluralize).new(contact_params)
14
15 if @contact.save
16 redirect_to @user, notice: 'Contact was successfully created.'
17 else
18 render :new
19 end
20 end
21
22 # GET /contacts/1/edit
23 def edit; end
24
25 # PATCH/PUT /contacts/1
26 def update
27 if @contact.update(contact_params)
28 redirect_to @user, notice: 'Contact was successfully updated.'
29 else
30 render :edit
31 end
32 end
33
34 # DELETE /contacts/1
35 def destroy
36 @contact.destroy
37 redirect_to @user, notice: 'Contact was successfully destroyed.'
38 end
39
40 private
41
42 # Use callbacks to share common setup or constraints between actions.
43 def set_contact
44 @user = User.find(params[:user_id])
45 @contact = @user.send(set_type.pluralize).find(params[:id])
46 end
47
48 def set_type
49 case params[:type]
50 when 'Friend'
51 'friend'
52 when 'Emergency'
53 'emergency'
54 end
55 end
56
57 # Only allow a list of trusted parameters through.
58 def contact_params
59 params.require(set_type.to_sym).permit(
60 :user_id, :type, :first_name, :last_name, :phone_number, :address,
61 :city, :state, :birthday
62 )
63 end
64end

Routes

Pada routes, saya akan menggunakan namespace untuk :users.

config/routes.rb
1Rails.application.routes.draw do
2 root to: 'users#index'
3
4 resources :users do
5 resources :friends, controller: :contacts, type: 'Friend'
6 resources :emergencies, controller: :contacts, type: 'Emergency'
7 end
8end

Dari 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.

app/views/users/show.html.erb
1...
2...
3
4<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>
22
23<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.

app/views/users/show/_table_body.html.erb
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.

app/views/contacts/_form.html.erb
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

  1. www.driftingruby.com/episodes/single-table-inheritance
    Diakses tanggal: 2020/02/21

  2. guides.rubyonrails.org/routing.html#controller-namespaces-and-routing
    Diakses tanggal: 2020/02/21

  3. api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
    Diakses tanggal: 2020/02/21