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 tabelcomments: 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')
morphTo()dipasang di sisi komentar. Artinya "milik siapa pun yang tertulis dicommentable_typedancommentable_id".$comment->commentablebisa mengembalikanPostatauVideo.morphMany(Comment::class, 'commentable')dipasang di sisi pemilik.'commentable'adalah awalan nama kedua kolom tadi.$post->commentsmengirim queryWHERE commentable_type = ... AND commentable_id = 5.- Di migration,
$table->morphs('commentable');membuat kedua kolom sekaligus index-nya. - Secara bawaan, Laravel mengisi
commentable_typedengan nama class lengkap,App\Models\Post. DenganRelation::enforceMorphMap(['Post' => Post::class, 'Video' => Video::class]);diAppServiceProvider, yang disimpan alias pendek'Post', seperti di contoh ini. Datanya tetap benar walau class-nya nanti dipindah atau diganti nama.
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))
);
- Tiap kolom punya foreign key sungguhan.
CHECKbernamaone_ownermemastikan tepat satu yang terisi: komentar untuk post dan video sekaligus ditolak denganCHECK constraint failed: one_owner. DELETE FROM posts WHERE id = 5;sekarang ikut menghapus komentarnya (ON DELETE CASCADE). Komentar untuk post #99 ditolak:FOREIGN KEY constraint failed.- SQLite baru mengecek foreign key setelah
PRAGMA foreign_keys = ON;. MySQL (InnoDB) dan PostgreSQL selalu mengecek. - Harganya: pemilik baru (misalnya produk) berarti kolom baru, migration baru, dan
CHECKyang makin panjang.
| 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
- Filter cuma pakai
commentable_id. Komentar Post #5 dan Video #5 tercampur. Selalu pakai kedua kolom. - Lupa index gabungan. Tanpa index di
(commentable_type, commentable_id), setiap$post->commentsmembaca seluruh tabel.morphs()sudah membuatnya. - Mengira database membersihkan komentar. Hapus post gak menghapus komentarnya. Hapus sendiri di aplikasi.
- Format di latihan. Kembalikan persis
commentable_type = 'Post' AND commentable_id = 5: tipe di dalam kutip tunggal, id tanpa kutip, tanpa kataWHEREdi depannya.
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.