بسم الله الرحمن الرحيم

Prerequisite

Ruby 2.6.3 Rails 5.2.3 PostgreSQL 11.5

Prakata

Counter Cache, apa itu?

Merupakan field/kolom yang akan menyimpan hasil perhitungan dari tabel yang berasosiasi dengan dirinya (tabel yang memiliki field/kolom counter cache).

Dengan menggunakan counter cache, kita akan memperoleh keuntungan dimana object yang kita buat tidak perlu memanggil database untuk mendapatkan suatu nilai total, misal COUNT(*) query, jadi cukup dengan mengakses counter cache field saja.

Contohnya kalau dalam Rails project seperti ini.

@user.posts.size

Atau

@user.posts.count

Kedua Active Record di atas akan menghasilkan SQL query serperti ini kira-kira.

SELECT COUNT(*) FROM "articles" WHERE "articles"."author_id" = $1

Nah, penggunaan size() dan count() ini akan menambah query COUNT(*) yang artinya akan menambahkan query baru untuk memanggil database.

Namun, pada catatan ini, saya tidak membahas secara mendalam mengenai Counter Cache, teman-teman dapat membacanya pada blog dari teman-teman yang lain.

Saya akan membahas kasus yang mungkin cukup unik, yang saya alami.

Permasalahan

Seperti definisi yang sudah saya jelaskan di atas. Counter Cache adalah salah satu options dari banyak options yang dapat kita gunakan apabila kita menggunakan Active Record Association yaitu belongs_to.1.

Active Record Association, sesuai namanya, Association, adalah hubungan/koneksi antara 2 Active Record model.

Yang artinya options counter cache dapat kita gunakan pada model yang saling berasosiasi.

Nah, sedangkan, saya ingin menggunakannya hanya pada 1 model.

Apakah bisa? Tentu saja bisa. Saya cukup lama mencari solusi ini.

Sekenario

Saya ingin membuat fitur “Invite Friend” yang nantinya akan terdapat 2 buah field.

  1. Field inviting_user_id, akan menampung data berupa id user yang menginvite user tersebut.
  2. Field inviting_users_count, akan menampung berapa banyak user yang berhasil diinvite.

Di sini, saya tidak menggunakan model lain selain model user.

Yang mana, seharusnya, untuk dapat menggunakan counter_cache, kita harus memiliki dua model yang saling berasosiasi menggunakan belongs_to.

Pemecahan Masalah

Pertama-tama seperti halnya convetion pada counter cache, saya perlu manambahkan kolom baru pada tabel yang ingin saya buatkan counter cachenya.

Buat migration untuk menambahkan kolom counter cache.

$ rails g migration add_inviting_user_to_users
    create 20191122174434_add_inviting_user_count_to_users.rb

Setelah file migrasi jadi, saya akan menambahkan dua buah kolom.

FILEdb/migrate/20191122174434_add_inviting_user_count_to_users.rb
1
2
3
4
5
6
class AddInvitingUsersCountToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :inviting_user_id, :integer
    add_column :users, :inviting_users_count, :integer, default: 0, null: false
  end
end

Untuk field inviting_users_count adalah field yang saya siapkan untuk counter cache, yang harus mengikuti aturan penamaan kolom untuk counter cache. yaitu, penamaannya harus plural (jamak).

Kemudian, jalankan migrationnya.

$ rails db:migrate

Apabila berhasil, berikut ini adalah bentuk dari skema database setelah migrasi berhasil kita jalankan.

FILEdb/schema.rb
1
2
3
4
5
6
7
create_table "users", force: :cascade do |t|
  ...
  ...
  ...
  t.integer "inviting_user_id"
  t.integer "inviting_users_count", default: 0, null: false
end
# Users table
+------------------------+-----------------------------+-----------------------------------------------------+
| Column                 | Type                        | Modifiers                                           |
|------------------------+-----------------------------+-----------------------------------------------------|
| id                     | bigint                      |  not null default nextval('users_id_seq'::regclass) |
| email                  | character varying           |  not null default ''::character varying             |
| encrypted_password     | character varying           |  not null default ''::character varying             |
| created_at             | timestamp without time zone |  not null                                           |
| updated_at             | timestamp without time zone |  not null                                           |
| full_name              | character varying           |                                                     |
| inviting_user_id       | integer                     |                                                     |
| inviting_users_count   | integer                     |  not null default 0                                 |
+------------------------+-----------------------------+-----------------------------------------------------+

Selanjutnya tinggal membuat asosiasi pada user model.

FILEapp/models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User < ApplicationRecord
  # ...
  # ...

  has_many   :inviting_users,
             class_name: 'User',
             foreign_key: :inviting_user_id
  belongs_to :inviting_user,
             class_name: 'User',
             counter_cache: :inviting_users_count,
             optional: true

  # ...
  # ...
end

Saya rasa, sudah jelas dari kode di atas. Bagaimana relasi antar kedua field dalam satu model dapat terjadi.

Kira-kira begini cerita asosiasi yang terjadi.

User memiliki banyak :inviting_user, yang berasal dari User class, yang akan ditempatkan pada field :inviting_user_id.

Data pada :inviting_user merupakan data milik User, yang berasal dari User class, yang akan ditempatkan pada field :inviting_users_count, asosiasi ini bersifat optional: true, sehingga, apabila tidak terdapat asosiasi dengan object :inviting_user, data user tetap akan dibuat.

Tujuan dari optional: true adalah untuk melewati validasi presence apabila data baru akan dibuat. Karena secara default pada Rails 5, belongs_to akan bernilai optional: false.

Apabila tidak, maka user baru yang tidak memiliki relasi dengan user lain, tidak akan dapat dibuat.

Begini kira-kira hasilnya.

# Users table
+------+-----------------------+--------------------+------------------------+
| id   | full_name             | inviting_user_id   | inviting_users_count   |
|------+-----------------------+--------------------+------------------------|
| 1    | Rizqi Assyaufi        | <null>             | 1                      |
| 2    | Baik Budiman          | 1                  | 0                      |

gambar_1

Gambar 1 - Baik Budiman melakukan registrasi dengan menggunakan kode referral yang diberikan oleh Rizqi Assyaufi.

Selesai!

Mudah-mudahan catatan kali ini dapat bermanfaat bagi teman-teman yang memerlukan.

Dokumentasi lebih lengkap dapat dibaca pada daftar referensi yang saya sertakan di bawah.

Terima kasih

(^_^)

Referensi

  1. guides.rubyonrails.org/association_basics.html#options-for-belongs-to
    Diakses tanggal: 2019/12/13

  2. guides.rubyonrails.org/association_basics.html#options-for-has-many-counter-cache
    Diakses tanggal: 2019/12/13

  3. stackoverflow.com/questions/35265225/rails-counter-cache-on-the-same-model
    Diakses tanggal: 2019/12/13


Penulis

bandithijo

My journey kicks off from reading textbooks as a former Medical Student to digging bugs as a Software Engineer – a delightful rollercoaster of career twists. Embracing failure with the grace of a Cat avoiding water, I've seamlessly transitioned from Stethoscope to Keyboard. Armed with ability for learning and adapting faster than a Heart Beat, I'm on a mission to turn Code into a Product.

- Rizqi Nur Assyaufi

d98d8237fef8f1017d0be931b6e291341cbe6ca8