The Lazy Eager Loading Paradox: N+1 Query Patterns dalam Polymorphic Relationships Laravel
Polymorphic relationships di Laravel bisa menciptakan hidden N+1 query problem bahkan saat menggunakan eager loading. Pelajari cara mengidentifikasi dan mengatasinya.
Pengenalan: Ketika Eager Loading Justru Membuat Segalanya Lebih Lambat
Dalam dunia Laravel, eager loading adalah pahlawan yang menyelamatkan aplikasi dari N+1 query problem. Namun, ada satu situasi tersembunyi yang jarang dibicarakan di forum dan tutorial mainstream: ketika kombinasi lazy loading, eager loading, dan polymorphic relationships bersatu, mereka menciptakan sebuah paradoks performa yang sangat halus namun merusak.
Paradoks ini terjadi ketika developer secara otomatis mengasumsikan bahwa eager loading selalu lebih baik tanpa mempertimbangkan kompleksitas dari polymorphic relationships. Hasilnya adalah aplikasi yang tampak menggunakan best practices, namun kenyataannya menghasilkan ratusan query yang tidak perlu.
Memahami Polymorphic Relationships di Laravel
Polymorphic relationships memungkinkan model untuk berhubungan dengan beberapa model lain menggunakan satu relasi. Contoh klasiknya adalah sistem comment di mana comment bisa dilekatkan pada berbagai jenis konten—artikel, video, foto, atau post.
Struktur database untuk polymorphic relationships menggunakan dua kolom khusus: commentable_type dan commentable_id. Ini adalah solusi elegan, namun memiliki implikasi performa yang tidak jelas bagi banyak developer.
The N+1 Problem dalam Polymorphic Context
Mari kita lihat sebuah skenario nyata. Anda memiliki model Activity yang mencatat semua aktivitas dalam sistem, dan activity dapat "dipicu" oleh berbagai jenis model: User, Post, atau Comment.
class Activity extends Model
{
public function triggerable()
{
return $this->morphTo();
}
}
class User extends Model
{
public function activities()
{
return $this->morphMany(Activity::class, 'triggerable');
}
}
class Post extends Model
{
public function activities()
{
return $this->morphMany(Activity::class, 'triggerable');
}
}
Sekarang, jika Anda ingin mengambil 100 activities dan melakukan eager loading relationship-nya:
$activities = Activity::with('triggerable')->limit(100)->get();
Apa yang terjadi di balik layar? Laravel akan menjalankan 1 query untuk mengambil activities. Namun kemudian, Laravel harus menentukan tipe dari setiap triggerable. Jika activities tersebut merujuk pada mix dari User, Post, dan Comment dengan distribusi yang berbeda, Laravel akan menjalankan query terpisah untuk masing-masing tipe—bukan 1 query lagi, melainkan 3+ queries tambahan.
Dalam skenario di mana Anda memiliki 100 activities dengan 10 tipe berbeda, Anda bisa berakhir dengan 11 queries. Ini masih jauh lebih baik daripada 100 queries (jika lazy loading), tetapi ini bukan solusi optimal.
The Lazy Eager Loading Paradox Explained
Paradoks muncul ketika developer menggunakan lazy loading dengan harapan mengoptimalkan—sebuah pendekatan kontra-intuitif yang disebut "lazy eager loading".
$activities = Activity::limit(100)->get();
// Kemudian, di view atau logic:
foreach ($activities as $activity) {
echo $activity->triggerable->name; // Ini memicu query!
}
Dalam contoh di atas, Anda mendapatkan 100 queries—satu untuk setiap activity yang mengakses relationshipnya. Ini adalah N+1 problem klasik yang semua orang tahu.
Namun, ada situasi yang lebih halus—ketika Anda menggunakan eager loading tetapi ada conditional logic atau nested relationships yang tidak Anda ketahui:
$activities = Activity::with('triggerable')->limit(100)->get();
foreach ($activities as $activity) {
if ($activity->triggerable_type === 'App\\Models\\User') {
echo $activity->triggerable->email; // Ini adalah query terpisah untuk USER
} elseif ($activity->triggerable_type === 'App\\Models\\Post') {
echo $activity->triggerable->title; // Query terpisah untuk POST
}
}
Hasilnya adalah Anda memiliki eager loading untuk polymorphic relationship, tetapi aplikasi masih menjalankan banyak queries. Ini adalah paradoks: Anda menggunakan best practice (eager loading), tetapi performa masih buruk karena sifat polymorphic relationships.
Mengatasi Paradoks: Solusi Teknis
Solusi 1: Segmentasi berdasarkan Type
Alih-alih eager loading semua polymorphic relationships sekaligus, segmentasi activities berdasarkan type-nya terlebih dahulu:
$activities = Activity::limit(100)->get();
$grouped = $activities->groupBy('triggerable_type');
foreach ($grouped as $type => $items) {
$related = $type::find($items->pluck('triggerable_id'));
// Sekarang Anda hanya melakukan 1 query per type
}
Solusi 2: Menggunakan Query Builder dengan Union
Untuk kasus yang lebih kompleks, pertimbangkan menggunakan union queries yang mengambil semua triggerables dalam satu query besar:
$activities = Activity::limit(100)->get();
forest $type => $items) {
${$type} = DB::table(strtolower(class_basename($type)))
->whereIn('id', $items->pluck('triggerable_id'))
->get()
->keyBy('id');
}
Solusi 3: Select Specific Columns
Jika Anda hanya membutuhkan beberapa kolom dari related models, gunakan eager loading dengan select() untuk membatasi data yang di-fetch:
$activities = Activity::with([
'triggerable' => function ($query) {
$query->select('id', 'name', 'email');
}
])->limit(100)->get();
Best Practices untuk Polymorphic Relationships
- Selalu gunakan
with()untuk eager loading, jangan pernah lazy load dalam loop - Monitor query count menggunakan Laravel Debugbar atau Log untuk mendeteksi N+1 problems
- Pertimbangkan denormalisasi data—store nilai penting dari related model langsung di polymorphic table jika perlu
- Gunakan query caching untuk hasil yang jarang berubah
- Test performa dengan dataset besar—jangan hanya test dengan 10-20 records
Kesimpulan: Memahami Trade-offs
Paradoks lazy eager loading dalam polymorphic relationships adalah bukti bahwa tidak ada solusi one-size-fits-all dalam database optimization. Eager loading bukan selalu jawaban—kadang Anda perlu memahami struktur data Anda secara mendalam dan mengambil pendekatan yang lebih strategis.
Kunci adalah: mengukur, memahami, dan mengoptimalkan berdasarkan data nyata, bukan hanya mengandalkan best practices yang generic. Polymorphic relationships memberikan fleksibilitas arsitektural, tetapi dengan harga yang harus dibayar dalam kompleksitas query optimization.