Membedah Active Record: model sebagai class Ruby di atas tabel, associations has_many/belongs_to/has_many :through, validations, scopes, dan query chaining dengan membangun model Blog — Post, Comment, dan User

Setelah di episode 4 kalian menguasai routing dan controller, episode 5 membahas lapisan yang paling sering dipakai sehari-hari: Active Record — ORM bawaan Rails yang menjadi jembatan antara objek Ruby dan baris database. Inilah "model gemuk" yang di episode 2 kita janjikan akan kita pelajari.
Mengapa Active Record penting dikuasai dalam? Karena hampir semua query aplikasi — dari mencari user sampai agregasi laporan — ditulis dalam bahasa model ini. Memahami relasi (has_many/belongs_to), validations, dan scopes berarti kalian bisa mengekspresikan aturan bisnis di tempat yang tepat: di model, bukan berserakan di controller dan view.
Di episode 3 kita melihat Post dari scaffold. Model Rails adalah class Ruby yang mewarisi ApplicationRecord (yang mewarisi ActiveRecord::Base):
class Post < ApplicationRecord
endHanya dengan ini, Post sudah punya kemampuan CRUD penuh: Post.all, Post.find(1), post.save, post.destroy. Nama class Post dipetakan ke tabel posts oleh konvensi — tidak ada config mapping yang perlu ditulis.
Sekarang kita bangun relasi blog klasik: Post, Comment, dan User. Pertama buat tabelnya (migration akan dibahas penuh di episode 6):
bin/rails g model User name:string email:string
bin/rails g model Post user:references title:string body:text
bin/rails g model Comment user:references post:references body:text
bin/rails db:migrateuser:references dan post:references membuat kolom foreign key sekaligus belongs_to otomatis di model. Sekarang definisikan relasi di model:
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
end
class Comment < ApplicationRecord
belongs_to :user
belongs_to :post
endRelasi ini menghasilkan method otomatis: post.comments, post.user, user.posts, comment.post. dependent: :destroy memastikan saat post dihapus, comment-nya ikut terhapus — mencegah orphan rows.
Untuk relasi banyak-ke-banyak, gunakan join model:
class User < ApplicationRecord
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
class Group < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
enduser.groups kini berjalan lewat tabel memberships — pola standar untuk relasi banyak-ke-banyak dengan data tambahan (misal role di dalam membership).
Validations berjalan di level model, dijalankan sebelum save:
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { maximum: 120 }
validates :body, presence: true
validates :title, uniqueness: { scope: :user_id }
endBeberapa validation umum: presence, length, uniqueness (bisa di-scope), numericality, format, dan inclusion. Saat validasi gagal, post.save mengembalikan false dan post.errors berisi detail:
post = Post.new(title: "")
post.valid? # => false
post.errors[:title] # => ["can't be blank"]
post.save # => falseTip
Validasi uniqueness dijalankan dengan query database, sehingga membutuhkan unique index di migration untuk benar-benar aman dari race condition (dua request simultan). Kebiasaan baik: selalu pasang add_index ... unique: true bersama validasi uniqueness.
Scope adalah filter query bernama yang bisa di-chain:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by, ->(user) { where(user: user) }
endPemakaian: Post.published.recent.by(user). Karena scope mengembalikan relation, scope bisa di-chain tanpa batas dan tidak mengeksekusi query sampai benar-benar dibutuhkan.
Kunci memahami Active Record: query tidak dieksekusi sampai dipaksa (misal dengan .each, .to_a, .count). Rantai method membangun SQL secara bertahap:
posts = Post.published.recent.limit(10)
posts.loaded? # => false (belum query)
posts.to_a # → eksekusi SQLAlasan "mengapa" ini penting: kita bisa menambahkan filter berdasarkan kondisi runtime tanpa membangun ulang query, dan bisa memakai relasi yang sama untuk menghitung jumlah baris tanpa memuat data penuh (posts.count vs posts.to_a.size).
Contoh praktik lengkap di console:
u = User.create(name: "Rina", email: "rina@example.com")
p = u.posts.create(title: "Belajar Rails", body: "Episode 5")
u.posts.published.recent.count
p.comments.create(user: u, body: "Komentar pertama")
p.comments.map(&:body)Perhatikan bahwa u.posts.create(...) otomatis mengisi user_id — salah satu kenyamanan associations.
dependent: :destroy — menghapus post meninggalkan comment dengan foreign key mengarah ke baris yang hilang; query bergabung jadi aneh dan integritas rusak.includes vs joins — joins untuk memfilter (INNER JOIN), includes untuk memuat relasi sekaligus (hindari N+1). Dibedah detail di episode 12.Post.where(...).map mengembalikan array sehingga tidak bisa di-chain lagi. Gunakan relation selama mungkin.uniqueness saja tidak cukup di database ber-load tinggi.Episode 5 memperkenalkan Active Record sebagai ORM Rails: model = class Ruby di atas tabel, associations menghubungkan tabel dengan method otomatis, validations menegakkan aturan bisnis sebelum save, scopes merapikan query reusable, dan query chaining bekerja secara lazy.
Inti yang harus dibawa pulang:
ApplicationRecord; konvensi nama memetakan class ke tabel.belongs_to, has_many, has_many :through, selalu dengan dependent yang tepat.Di episode 6 selanjutnya kita akan membedah migrations & schema — bin/rails db:migrate, rollback, schema.rb, seed data, dan praktik schema evolution di production. Sampai jumpa di episode 6!