Salah satu hal yang bikin Laravel terasa menyenangkan adalah Eloquent. Kamu tidak perlu nulis JOIN manual — cukup definisikan hubungan antar model, dan Laravel yang urus sisanya.
Tapi ada tujuh jenis relasi, dan masing-masing punya konteks pemakaian yang berbeda. Kalau dipelajari satu-satu tanpa gambaran besarnya, mudah sekali bingung — terutama ketika sampai di bagian polymorphic.
Jadi kita mulai dari yang paling simpel, naik pelan-pelan ke yang paling kompleks. Untuk setiap relasi, kita pakai satu kasus nyata yang konkret supaya mudah dibayangkan.
🧭 Peta Relasi Sebelum Mulai
Sebelum masuk ke kode, ini gambaran besar ketujuh relasi berdasarkan strukturnya:
Satu ke Satu hasOne / belongsTo
Satu ke Banyak hasMany / belongsTo
Banyak ke Banyak belongsToMany (dua arah)
Satu ke Banyak Lewat hasManyThrough
Polymorphic 1:1 / 1:N morphOne / morphMany / morphTo
Polymorphic N:N morphToMany / morphedByMany
1️⃣ One to One — hasOne & belongsTo
Kasus nyata
Satu User punya tepat satu Profile. Satu Profile hanya milik satu User.
Ini biasanya terjadi ketika kamu memisahkan data autentikasi (email, password) dari data profil (bio, avatar, nomor telepon) — dua concern yang berbeda, jadi dua tabel yang berbeda.
Struktur tabel
users
id (PK)
email
profiles
id (PK)
user_id (FK → users.id) ← foreign key ada di sisi "anak"
bio
avatar
Kode model
📁 app/Models/User.php
class User extends Model
{
// "Saya punya satu profile di tabel lain"
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
}
📁 app/Models/Profile.php
class Profile extends Model
{
// "Saya punya foreign key — saya milik seorang User"
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Cara pakainya
// Ambil profile dari user
$profile = User::find(1)->profile;
// Ambil user pemilik profile
$user = Profile::find(1)->user;
📌 Aturan mudah: Foreign key (user_id) selalu ada di tabel yang belongsTo. Kalau bingung mana yang hasOne dan mana yang belongsTo — cari tabel mana yang punya kolom _id-nya, itulah yang belongsTo.
2️⃣ One to Many — hasMany & belongsTo
Kasus nyata
Satu Post bisa punya banyak Comment. Tapi satu Comment hanya milik satu Post.
Ini relasi paling umum. Hampir semua entitas “induk–anak” cocok dengan pola ini: User → Orders, Category → Products, Invoice → Items.
Struktur tabel
posts
id (PK)
title
comments
id (PK)
post_id (FK → posts.id) ← foreign key tetap di sisi "anak"
body
user_id
Kode model
📁 app/Models/Post.php
class Post extends Model
{
// "Saya punya banyak komentar"
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
📁 app/Models/Comment.php
class Comment extends Model
{
// "Saya milik sebuah Post"
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
Cara pakainya
// Semua komentar dari sebuah post
$comments = Post::find(1)->comments;
// Post mana yang dimiliki komentar ini
$post = Comment::find(1)->post;
// Buat komentar baru langsung lewat relasi
Post::find(1)->comments()->create([
'body' => 'Artikel yang bagus!',
'user_id' => auth()->id(),
]);
📌 Perbedaan ->comments (tanpa kurung) dan ->comments() (dengan kurung): yang pertama langsung mengembalikan koleksi, yang kedua mengembalikan query builder — bisa di-chain dengan where(), orderBy(), dll.
3️⃣ Many to Many — belongsToMany
Kasus nyata
Seorang User bisa punya banyak Role (admin, editor, viewer). Dan satu Role bisa dimiliki oleh banyak User.
Ini tidak bisa diselesaikan hanya dengan dua tabel — karena tidak ada yang bisa menaruh foreign key di satu pihak saja. Dibutuhkan tabel pivot sebagai perantara.
Struktur tabel
users
id (PK)
roles
id (PK)
name
role_user ← tabel pivot (urutan alfabetis, snake_case)
user_id (FK → users.id)
role_id (FK → roles.id)
Kode model
📁 app/Models/User.php
class User extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
// Laravel otomatis cari tabel pivot 'role_user'
}
}
📁 app/Models/Role.php
class Role extends Model
{
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}
Cara pakainya
$user = User::find(1);
// Lampirkan role ke user (INSERT ke tabel pivot)
$user->roles()->attach($roleId);
// Lepas role dari user (DELETE dari tabel pivot)
$user->roles()->detach($roleId);
// Sinkronisasi — setel ulang pivot agar hanya berisi ID ini
// Role yang tidak ada di array akan otomatis dilepas
$user->roles()->sync([1, 3, 5]);
// Ambil semua role milik user
foreach ($user->roles as $role) {
echo $role->name;
}
📌 Kalau tabel pivot punya kolom tambahan (misalnya assigned_at, level), akses lewat properti pivot:
// Definisikan kolom ekstra di model
return $this->belongsToMany(Role::class)->withPivot('assigned_at');
// Akses nilainya
echo $user->roles->first()->pivot->assigned_at;
4️⃣ Has Many Through — hasManyThrough
Kasus nyata
Sebuah Country ingin mengambil semua Post yang ditulis oleh warganya. Tapi Country tidak punya relasi langsung ke Post — hanya punya relasi ke User, dan User yang punya relasi ke Post.
Ini yang disebut relasi “lewat” — kamu butuh model perantara untuk menyambungkan dua model yang tidak terhubung langsung.
Struktur tabel
countries
id (PK)
name
users
id (PK)
country_id (FK → countries.id)
posts
id (PK)
user_id (FK → users.id)
title
Kode model
📁 app/Models/Country.php
class Country extends Model
{
// "Saya punya banyak Post, lewat User"
public function posts(): HasManyThrough
{
return $this->hasManyThrough(
Post::class, // model tujuan akhir
User::class // model perantara
);
}
}
Cara pakainya
// Semua post dari user yang berasal dari Indonesia
$posts = Country::where('name', 'Indonesia')->first()->posts;
📌 hasManyThrough tidak butuh kode tambahan di model User atau Post. Cukup definisikan di model Country, Laravel yang urus query-nya secara otomatis.
5️⃣ Polymorphic One to Many — morphMany & morphTo
Kasus nyata
Fitur komentar di aplikasimu bisa menempel ke Post maupun ke Video. Kamu tidak mau bikin tabel post_comments dan video_comments yang strukturnya identik — itu duplikasi.
Polymorphic adalah solusinya: satu tabel comments yang bisa “menempel” ke model mana saja.
Struktur tabel
comments
id (PK)
body
commentable_id ← ID dari model induknya (Post atau Video)
commentable_type ← nama class model induknya ('App\Models\Post')
Kode model
📁 app/Models/Post.php
class Post extends Model
{
// "Saya bisa punya banyak komentar (polymorphic)"
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
📁 app/Models/Video.php
class Video extends Model
{
// Persis sama — polymorphic bisa ditempel ke model apa saja
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
📁 app/Models/Comment.php
class Comment extends Model
{
// "Saya bisa milik model apa saja — Post, Video, atau yang lain"
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
Cara pakainya
// Tambah komentar ke post
Post::find(1)->comments()->create(['body' => 'Mantap!']);
// Tambah komentar ke video — kode yang sama persis
Video::find(1)->comments()->create(['body' => 'Keren!']);
// Dari komentar, ambil model induknya (entah Post atau Video)
$comment = Comment::find(1);
$parent = $comment->commentable; // bisa berupa Post atau Video
📌 commentable adalah nama bebas — itu yang disebut morph name. Harus konsisten antara nama di kolom tabel (commentable_id, commentable_type) dan nama yang dipakai di morphMany('commentable') dan morphTo().
6️⃣ Polymorphic Many to Many — morphToMany & morphedByMany
Kasus nyata
Fitur Tag di aplikasimu bisa dipakai di Post, Video, maupun Product. Dan satu konten bisa punya banyak tag. Ini Many-to-Many, tapi polymorphic — karena tag bisa menempel ke model yang berbeda-beda.
Struktur tabel
tags
id (PK)
name
taggables ← tabel pivot polymorphic
tag_id (FK → tags.id)
taggable_id ← ID dari Post, Video, atau Product
taggable_type ← nama class-nya
Kode model
📁 app/Models/Post.php
class Post extends Model
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
📁 app/Models/Tag.php
class Tag extends Model
{
// Dari Tag, ambil semua Post yang pakai tag ini
public function posts(): MorphedByMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
// Dari Tag, ambil semua Video yang pakai tag ini
public function videos(): MorphedByMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
Cara pakainya
// Tempel tag ke post
Post::find(1)->tags()->attach($tagId);
// Ambil semua post dengan tag tertentu
$posts = Tag::where('name', 'laravel')->first()->posts;
🗺️ Gambaran Besar — Kapan Pakai Yang Mana
Pertanyaan pertama: berapa "banyak" di setiap sisi?
│
├── Satu ↔ Satu
│ └── hasOne + belongsTo
│ Contoh: User ↔ Profile
│
├── Satu ↔ Banyak
│ └── hasMany + belongsTo
│ Contoh: Post → Comments
│
├── Banyak ↔ Banyak
│ └── belongsToMany (butuh tabel pivot)
│ Contoh: User ↔ Roles
│
├── Tidak ada relasi langsung, lewat model lain
│ └── hasManyThrough
│ Contoh: Country → User → Posts
│
└── Relasi bisa menempel ke banyak model berbeda?
│
├── Satu / Banyak → morphOne / morphMany + morphTo
│ Contoh: Post & Video → Comments
│
└── Banyak ↔ Banyak → morphToMany + morphedByMany
Contoh: Post & Video & Product ↔ Tags
📊 Perbandingan Singkat
| Relasi | Method | Kebutuhan Tabel Tambahan | Polymorphic |
|---|---|---|---|
| One to One | hasOne / belongsTo | ❌ | ❌ |
| One to Many | hasMany / belongsTo | ❌ | ❌ |
| Many to Many | belongsToMany | ✅ pivot table | ❌ |
| Has Many Through | hasManyThrough | ❌ | ❌ |
| Poly One to Many | morphMany / morphTo | ❌ (2 kolom ekstra) | ✅ |
| Poly Many to Many | morphToMany / morphedByMany | ✅ pivot polymorphic | ✅ |
💡 Ingat!
Dua hal yang paling sering bikin bingung di relasi Eloquent:
Pertama — belongsTo selalu ada di model yang punya foreign key di tabelnya. Kalau kamu lihat kolom user_id di tabel posts, maka Post yang punya belongsTo(User::class) — bukan sebaliknya.
Kedua — Polymorphic bukan sesuatu yang perlu dipaksakan di setiap kasus. Pakai polymorphic hanya ketika satu entitas benar-benar perlu “menempel” ke beberapa model berbeda secara bersamaan. Kalau tidak ada kebutuhan itu, relasi biasa jauh lebih mudah dibaca dan di-debug.
Dan satu lagi — selalu gunakan eager loading (with()) ketika mengambil relasi dalam loop. Topik itu layak artikel sendiri, tapi intinya: Post::with('comments')->get() jauh lebih baik daripada memanggil $post->comments di dalam foreach.