Sebelum kita membanjiri semua query dengan with(), penting memahami bahwa lazy loading bukan bug melainkan perilaku bawaan Eloquent — dan ada kalanya perilaku itu justru tepat. Saat kita mengakses $post->user untuk pertama kalinya, Eloquent menjalankan query relasi saat itu juga, menyimpannya di memori, dan mengembalikan hasil yang sama untuk akses berikutnya. Masalah muncul bukan karena mekanismenya, melainkan karena frekuensinya: akses di dalam loop membuat query bertumpuk tanpa terlihat. Chapter ini membedah mekanisme tersebut langkah demi langkah dengan query log sebagai alat ukur, lalu menutupnya dengan cara menjadikan lazy loading sebagai pelanggaran yang langsung terdeteksi di lingkungan development.

Relasi Termuat Saat Disentuh Pertama Kali #

Kita amati perilaku bawaan dengan menghitung query pada setiap langkah:

php
use App\Models\Post;
use Illuminate\Support\Facades\DB;

DB::enableQueryLog();

$post = Post::first();              // query 1: tabel posts
echo 'log setelah first(): ', count(DB::getQueryLog()), "\n";

echo $post->user->name, "\n";       // query 2: tabel users
echo 'log setelah akses pertama: ', count(DB::getQueryLog()), "\n";

echo $post->user->email, "\n";     // tanpa query baru
echo 'log setelah akses kedua: ', count(DB::getQueryLog()), "\n";

Output yang Dihasilkan:

text
log setelah first(): 1
Budi
log setelah akses pertama: 2
[email protected]
log setelah akses kedua: 2

Query kedua baru berjalan ketika ekspresi $post->user dievaluasi — itulah makna “lazy” (malas): kerja ditunda sampai benar-benar dibutuhkan. Setelah hasilnya tersimpan di property relations, akses $post->user->email dan seterusnya dibebani dari memori tanpa query tambahan. Perilaku ini membuat kode tetap nyaman ditulis: kita tidak perlu mendeklarasikan di muka relasi mana yang bakal dipakai.

Kapan Lazy Loading Aman #

Untuk satu model tunggal, lazy loading nyaris selalu wajar. Ambil kasus route binding: controller menerima satu Post dari Route::model() lalu menampilkan penulisnya:

php
DB::flushQueryLog();

$post = Post::findOrFail(1);
echo 'Penulis: ', $post->user->name, "\n";
echo 'total query: ', count(DB::getQueryLog()), "\n";

Output yang Dihasilkan:

text
Penulis: Budi
total query: 2

Dua query untuk satu halaman detail tidak masalah — bahkan bila kita memuat tiga relasi berbeda dari model ini, totalnya tetap empat query yang berjalan sekali saja. Laravel 13 bahkan menulis pengecualian ini ke dalam implementasinya : flag pencegahan lazy loading hanya ditempelkan pada model ketika query menghasilkan lebih dari satu baris. Model tunggal hasil first() atau route binding lolos dari pemeriksaan tersebut.

Kapan Lazy Loading Merusak #

Situasi berubah total di dalam loop. Kita hitung lagi dengan enam post kita:

php
DB::flushQueryLog();

foreach (Post::orderBy('id')->get() as $post) {
    echo $post->judul, ' — ', $post->user->name, "\n";
}
echo 'total query: ', count(DB::getQueryLog()), "\n";

Output yang Dihasilkan:

text
Belajar Relasi 01 — Budi
Belajar Relasi 02 — Siti
Belajar Relasi 03 — Budi
Belajar Relasi 04 — Siti
Belajar Relasi 05 — Budi
Belajar Relasi 06 — Siti
total query: 7

Enam post menghasilkan enam query users tambahan. Pola 1 + N ini linear: halaman dengan 200 post menjalankan 201 query hanya untuk daftar penulis, dan setiap query punya biaya jaringan serta parsing yang dibayar pengguna. Tabel berikut merangkum kapan masing-masing strategi tepat:

SituasiStrategi tepatAlasan
Detail satu model (route binding)Lazy loadingPaling banyak 1 query per relasi, kode lebih ringkas
Loop atas koleksi modelEager loading with()Jumlah query konstan (2-3), tidak peduli jumlah baris
Relasi dipakai kondisional sajaloadMissing() saat dibutuhkanQuery hanya berjalan bila cabang kode dieksekusi
Hanya butuh jumlah relasiwithCount()Tidak memuat baris relasi ke memori

Melarang Lazy Loading di Development #

Karena bahaya lazy loading bersifat diam-diam, Laravel menyediakan saklar untuk menjadikannya error saat development:

php
use App\Models\Post;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\LazyLoadingViolationException;

Model::preventLazyLoading(true);

try {
    foreach (Post::orderBy('id')->get() as $post) {
        echo $post->user->name, "\n";
    }
} catch (LazyLoadingViolationException $e) {
    echo $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.

Pesan ini langsung menunjuk dua informasi yang kita butuhkan: model pelaku (App\Models\Post) dan relasi yang dimuat malas (user). Tempat pemasangan saklar yang lazim adalah method boot() di app/Providers/AppServiceProvider.php:

php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

Dengan kondisi ! $this->app->isProduction(), pelanggaran meledak sebagai exception di lokal dan testing, tetapi dimaafkan di production agar deploy lama tidak ikut rusak saat saklar dinyalakan.

Saklar ini juga punya versi yang lebih luas: Model::shouldBeStrict() sekaligus mengaktifkan pencegahan lazy loading, pencegahan atribut yang terbuang secara diam-diam oleh mass assignment, dan error saat mengakses atribut yang tidak diseleksi query. Filosofinya sederhana: selama development, semua kejanggalan sebaiknya keras dan cepat terlihat — bukan diam-diam menjadi utang performa. Setelah aturan main dimuatnya relasi jelas, kita lanjut ke chapter berikutnya: cara membangun kondisi query yang menembus relasi dengan whereHas().