Mengelola Relasi Database Kompleks di Laravel Eloquent
Mengapa Eloquent butuh pemahaman lebih dalam?
Eloquent adalah salah satu ORM paling elegan di dunia PHP. Tapi kemudahannya sering membuat developer lupa bahwa setiap method seperti with(), whereHas(), atau loadCount() sebenarnya diterjemahkan ke query SQL. Hasilnya: N+1 problem, performance bottleneck, dan controller yang membengkak.
Artikel ini adalah rangkuman pengalaman saya waktu menangani database dengan 50+ tabel dan relasi nested di project enterprise.
1. Eager Loading: Solusi N+1 Problem
Masalah
// 🚫 N+1 Problem: 100 users = 101 query!
$users = User::all();
foreach ($users as $user) {
echo $user->profile->city; // Trigger 1 query EACH iteration
}
Solusi: Eager Loading
// ✅ Hanya 2 query untuk 100 users
$users = User::with('profile')->get();
// SQL:
// SELECT * FROM users;
// SELECT * FROM profiles WHERE user_id IN (1, 2, 3, ... 100);
Nested Eager Loading
// User -> Post -> Comment -> Author
$users = User::with('posts.comments.author')->get();
// Selectif hanya kolom tertentu untuk save bandwidth
$users = User::with(['posts:id,user_id,title', 'posts.comments:id,post_id,body'])->get();
Conditional Eager Loading
// Load hanya jika user login sebagai admin
$users = User::when($isAdmin, function ($q) {
$q->with('auditLogs');
})->get();
2. whereHas vs where: Buat Query yang Tepat
whereHas menjalankan subquery EXISTS yang bisa berat:
// 🚫 Lambat kalau data besar
$users = User::whereHas('posts', function ($q) {
$q->where('status', 'published');
})->get();
// SQL: SELECT * FROM users WHERE EXISTS (
// SELECT 1 FROM posts WHERE posts.user_id = users.id AND status = 'published'
// )
Faster alternative gunakan JOIN:
// ✅ Jauh lebih cepat di data besar
$users = User::select('users.*')
->join('posts', 'posts.user_id', '=', 'users.id')
->where('posts.status', 'published')
->distinct()
->get();
3. Polymorphic Relationships
Polymorphic memungkinkan satu model berelasi ke beberapa model berbeda. Ideal untuk sistem tagging, comments, atau likes yang berlaku untuk banyak entitas.
// Migration
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->morphs('commentable'); // Otomatis: commentable_type + commentable_id
$table->timestamps();
});
// Model
class Comment extends Model {
public function commentable() {
return $this->morphTo();
}
}
class Post extends Model {
public function comments() {
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model {
public function comments() {
return $this->morphMany(Comment::class, 'commentable');
}
}
// Penggunaan
$post->comments; // Comments untuk post tertentu
$video->comments; // Comments untuk video tertentu
$comment->commentable; // Akses post ATAU video dari satu comment
4. Many-to-Many dengan Pivot Data
Contoh klasik: User <-> Role dengan kolom tambahan assigned_at.
// Pivot table
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->timestamp('assigned_at')->useCurrent();
$table->primary(['user_id', 'role_id']);
});
// Model User
public function roles() {
return $this->belongsToMany(Role::class)
->withPivot('assigned_at')
->using(RoleUser::class);
}
// Query dengan filter pivot
$user->roles()->wherePivot('assigned_at', '>', now()->subDays(7))->get();
5. Query Scope: Pisahkan Logic dari Controller
Controller harus menjadi traffic controller. Business logic termasuk query conditions masuk ke Model sebagai Scope.
class User extends Model {
// Local Scope
public function scopeActive($query) {
return $query->where('is_active', true)
->whereNull('deleted_at');
}
public function scopeWithRole($query, string $role) {
return $query->whereHas('roles', function ($q) use ($role) {
$q->where('name', $role);
});
}
// Dynamic Scope (parameter)
public function scopeOfAge($query, int $minAge) {
return $query->where('age', '>=', $minAge);
}
}
// Controller bersih
public function index() {
$users = User::active()
->withRole('admin')
->ofAge(18)
->with('profile')
->paginate(20);
return view('users.index', compact('users'));
}
6. Repository Pattern untuk Abstraction
Untuk project besar, isol.query access di layer Repository:
interface UserRepositoryInterface {
public function findActiveWithRoles(int $id): ?User;
public function searchBySkills(array $skills): Collection;
}
class EloquentUserRepository implements UserRepositoryInterface {
public function findActiveWithRoles(int $id): ?User {
return User::active()
->with(['roles', 'profile'])
->find($id);
}
public function searchBySkills(array $skills): Collection {
return User::with('skills')
->whereHas('skills', function ($q) use ($skills) {
$q->whereIn('name', $skills);
})
->get();
}
}
// Bind ke service provider
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
// Controller inject via DI
public function __construct(private UserRepositoryInterface $repo) {}
public function show(int $id) {
$user = $this->repo->findActiveWithRoles($id);
abort_unless($user, 404);
return view('users.show', compact('user'));
}
7. Query Performance Tips
Chunking untuk dataset besar
// Jangan ->get() kalau hasilnya 100k baris!
User::chunk(500, function ($users) {
foreach ($users as $user) {
// Process satu user
}
});
// Atau cursor untuk memory-efficient iteration
foreach (User::cursor() as $user) {
// Pada setiap iterasi, hanya 1 model di memori
}
Database Indexing
-- Foreign key otomatis perlu index
CREATE INDEX idx_posts_user_id_status ON posts(user_id, status);
CREATE INDEX idx_comments_commentable ON comments(commentable_type, commentable_id);
Kesimpulan
Eloquent elegan tapi tidak otomatis optimal. Lakukan praktik:
- Selalu eager load relasi yang akan diakses
- Pakai JOIN alih-alih whereHas untuk dataset besar
- Polymorphic untuk relasi lintas entitas
- Query Scope untuk menjaga controller ramping
- Repository Pattern untuk isol.query access
- Chunk/Cursor untuk data volume besar
- Indexing untuk kolom yang sering di-WHERE
Hasilnya: database query cepat, controller clean, dan 100x scalability dengan effort minimal.