Polymorphic Relations — Database

Apa itu Polymorphic Relation? Polymorphic relation memungkinkan satu tabel berelasi dengan beberapa tabel lain menggunakan kolom *_type dan *_id. Ini…

Polymorphic relation membuat satu tabel bisa jadi milik beberapa tabel lain sekaligus. Tabel comments bisa menyimpan komentar untuk artikel, untuk video, atau untuk produk, cukup dengan dua kolom: commentable_type (milik tabel apa) dan commentable_id (id-nya berapa). Mirip catatan yang ditempel di mading sekolah: tiap catatan menulis "untuk: Artikel 5" atau "untuk: Video 3". Praktis, satu papan untuk semua. Tapi petugas mading gak pernah mengecek apakah Artikel 5 masih ada. Kalau artikelnya dicopot, catatannya tetap menempel.

🧪 Tebakan cepat: artikel #5 di blog Kedai Senja punya 2 komentar. Admin menghapus artikelnya dengan DELETE FROM posts WHERE id = 5;. Berapa komentar untuk artikel #5 yang masih tersisa di tabel comments: 0 atau 2? Tebak dulu, jawabannya ada di bagian "Gak ada foreign key yang menjaga".

Tabel yang kita pakai

Kedai Senja sekarang punya blog. Ada artikel (posts) dan video resep (videos), dan pelanggan bisa berkomentar di keduanya. Tanpa polymorphic, kamu butuh dua tabel yang kolomnya sama persis, post_comments dan video_comments, lalu tabel ketiga lagi begitu produk juga bisa dikomentari.

Satu tabel comments untuk semua

CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE TABLE videos (id INTEGER PRIMARY KEY, title TEXT NOT NULL);
CREATE TABLE comments (
  id INTEGER PRIMARY KEY,
  body TEXT NOT NULL,
  commentable_type TEXT NOT NULL,  -- 'Post' atau 'Video'
  commentable_id INTEGER NOT NULL  -- id di tabel itu
);
CREATE INDEX comments_commentable_index ON comments (commentable_type, commentable_id);

INSERT INTO posts VALUES (5, 'Rahasia Kopi Susu Aren');
INSERT INTO videos VALUES (3, 'Cara Bikin Teh Tarik');
INSERT INTO comments VALUES
  (1, 'Bagus!', 'Post', 5),
  (2, 'Keren!', 'Video', 3),
  (3, 'Setuju!', 'Post', 5);
id body commentable_type commentable_id
1 Bagus! Post 5
2 Keren! Video 3
3 Setuju! Post 5

Komentar untuk artikel #5:

SELECT * FROM comments WHERE commentable_type = 'Post' AND commentable_id = 5;

Hasilnya baris 1 dan 3. Kedua kolom wajib ada di WHERE: commentable_id = 5 saja juga akan mengambil komentar untuk Video #5 kalau ada. Perhatikan bentuknya: nama tipe di dalam kutip tunggal ('Post'), id-nya angka tanpa kutip. Index di kedua kolom itu membuat pencariannya cepat: SEARCH comments USING INDEX comments_commentable_index (commentable_type=? AND commentable_id=?).

Polymorphic di Laravel

class Comment extends Model
{
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
// Video sama persis: morphMany(Comment::class, 'commentable')

Gak ada foreign key yang menjaga

Foreign key (REFERENCES) selalu menunjuk ke satu tabel. commentable_id kadang menunjuk ke posts, kadang ke videos, tergantung isi kolom lain. Jadi database gak bisa memasang foreign key di situ, dan gak ada yang menjaga isinya:

DELETE FROM posts WHERE id = 5;
SELECT * FROM comments WHERE commentable_type = 'Post' AND commentable_id = 5;

Hasilnya masih 2 baris. Itu jawaban tebakannya. Dua komentar itu sekarang menunjuk ke artikel yang sudah gak ada. Baris seperti ini disebut orphan (yatim). Database juga menerima komentar untuk artikel yang gak pernah ada:

INSERT INTO comments (body, commentable_type, commentable_id) VALUES ('Halo?', 'Post', 99);
-- diterima, padahal gak ada post #99. Salah ketik 'Psot' pun diterima.

Integritasnya harus dijaga aplikasi. Di Laravel, hapus komentarnya sendiri saat artikel dihapus, misalnya $post->comments()->delete(); sebelum $post->delete();.

Alternatif: kolom foreign key terpisah

Kalau pemiliknya cuma sedikit dan jarang bertambah, beri tiap pemilik kolomnya sendiri:

PRAGMA foreign_keys = ON;  -- khusus SQLite: nyalakan pengecekan foreign key

CREATE TABLE comments (
  id INTEGER PRIMARY KEY,
  body TEXT NOT NULL,
  post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
  video_id INTEGER REFERENCES videos(id) ON DELETE CASCADE,
  CONSTRAINT one_owner CHECK ((post_id IS NOT NULL AND video_id IS NULL)
      OR (post_id IS NULL AND video_id IS NOT NULL))
);
Polymorphic Kolom foreign key terpisah
Tambah pemilik baru Cukup tulis tipe baru Kolom dan migration baru
Foreign key Gak bisa Bisa, dijaga database
Komentar yatim Bisa terjadi Dicegah database
Cocok untuk Komentar, tag, log aktivitas di banyak jenis data Sedikit pemilik yang jarang bertambah, data yang wajib konsisten

📌 Many-to-many polymorphic

Tag yang bisa dipasang di artikel, video dan produk memakai tabel pivot polymorphic:

CREATE TABLE taggables (
  tag_id INTEGER NOT NULL REFERENCES tags(id),
  taggable_type TEXT NOT NULL,
  taggable_id INTEGER NOT NULL
);

tag_id tetap bisa punya foreign key, taggable_id gak bisa. Di Laravel, sisi pemilik memakai morphToMany(Tag::class, 'taggable'), sisi tag memakai morphedByMany(Post::class, 'taggable').

⚠️ Jebakan yang sering ketemu

TL;DR: Polymorphic relation menyimpan pemilik lewat dua kolom, commentable_type dan commentable_id, jadi satu tabel comments bisa milik posts, videos atau products. Ambil datanya dengan kedua kolom: WHERE commentable_type = 'Post' AND commentable_id = 5. Di Laravel, morphTo() di sisi komentar dan morphMany(Comment::class, 'commentable') di sisi pemilik. Kelemahan utamanya: gak bisa memakai foreign key, jadi database gak mencegah komentar untuk post yang sudah dihapus atau gak pernah ada. Kalau pemiliknya sedikit dan datanya wajib konsisten, pakai kolom foreign key terpisah seperti post_id dan video_id.

Yang akan kamu pelajari