← Semua pembelajaran / Laravel Nol → Enterprise
Fase 2 · Database & Eloquent

Query builder

Eloquent dibangun di atas query builder. Untuk laporan, agregasi, dan operasi massal, sering kali lapisan bawah inilah yang paling tepat — dan paling cepat.

Sumber asli laravel.com Resmi Rangkuman ~7 menit baca

Intisari

  • Semua nilai di-bind sebagai parameter, jadi kebal SQL injection selama kamu tidak menyambung string sendiri.
  • chunkById() untuk data besar. get() atas jutaan baris akan menghabiskan memori.
  • upsert() dan updateOrInsert() menggantikan pola cek-lalu-simpan yang rawan balapan.
  • Query builder mengembalikan objek stdClass, bukan model — tanpa event, tanpa cast, tanpa relasi.
  • ->toSql() dan ->dd() memperlihatkan SQL sebelum dijalankan.

Bentuk dasar

use Illuminate\Support\Facades\DB;

$produk = DB::table('produk')
    ->select('produk.id', 'produk.nama', 'kategori.nama as kategori')
    ->join('kategori', 'kategori.id', '=', 'produk.kategori_id')
    ->where('produk.stok', '>', 0)
    ->whereIn('produk.kategori_id', [1, 2, 3])
    ->whereBetween('produk.harga', [10_000, 100_000])
    ->whereNull('produk.deleted_at')
    ->orderByDesc('produk.created_at')
    ->limit(50)
    ->get();

Menyusun kondisi secara dinamis

$q = DB::table('produk');

// when(): hanya menambahkan klausa kalau syaratnya terpenuhi
$q->when($request->filled('q'), fn ($q) =>
        $q->where('nama', 'like', '%'.$request->input('q').'%'))
  ->when($request->filled('kategori'), fn ($q) =>
        $q->where('kategori_id', $request->integer('kategori')))
  ->when($request->boolean('tersedia'), fn ($q) =>
        $q->where('stok', '>', 0));

$hasil = $q->paginate(20);

when() menggantikan tumpukan if. Tanpa itu, filter dinamis berakhir sebagai belasan if ($request->has(...)) { $q->where(...); } yang sulit dibaca. Dengan when(), rangkaiannya tetap satu ekspresi.

Agregasi dan pengelompokan

DB::table('pesanan')->count();
DB::table('pesanan')->where('status', 'dibayar')->sum('total');
DB::table('pesanan')->avg('total');

$per_bulan = DB::table('pesanan')
    ->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as bulan, COUNT(*) as jumlah, SUM(total) as omzet')
    ->where('created_at', '>=', now()->subYear())
    ->groupBy('bulan')
    ->havingRaw('COUNT(*) > 10')
    ->orderBy('bulan')
    ->get();

Untuk laporan seperti ini, query builder lebih tepat daripada Eloquent: kamu tidak butuh model, event, atau relasi — kamu butuh satu query yang menghasilkan angka.

Data besar: chunkById, bukan get

// BAHAYA — memuat seluruh tabel ke memori
foreach (DB::table('pesanan')->get() as $p) { /* ... */ }

// BENAR — 1.000 baris per putaran
DB::table('pesanan')->orderBy('id')->chunkById(1000, function ($baris) {
    foreach ($baris as $p) { /* ... */ }
});

// Lebih hemat lagi: satu baris pada satu waktu, kursor tetap terbuka
foreach (DB::table('pesanan')->orderBy('id')->cursor() as $p) { /* ... */ }

chunkById(), bukan chunk(). chunk() memakai OFFSET, jadi kalau kamu mengubah baris yang sedang diiterasi (misalnya mengisi kolom yang jadi syarat filter), sebagian baris akan terlewat karena posisinya bergeser. chunkById() menelusuri berdasarkan primary key dan kebal terhadap masalah itu.

Tulis massal

// Satu INSERT untuk banyak baris — jauh lebih cepat daripada perulangan create()
DB::table('log_impor')->insert([
    ['berkas' => 'a.csv', 'baris' => 120],
    ['berkas' => 'b.csv', 'baris' => 340],
]);

// Insert kalau baru, update kalau sudah ada — satu query
DB::table('stok_harian')->upsert(
    values: [
        ['produk_id' => 1, 'tanggal' => '2026-08-12', 'jumlah' => 10],
        ['produk_id' => 2, 'tanggal' => '2026-08-12', 'jumlah' => 4],
    ],
    uniqueBy: ['produk_id', 'tanggal'],     // butuh unique index di kolom ini
    update: ['jumlah'],
);

upsert() menghapus satu kelas balapan. Pola "cek dulu, kalau tidak ada baru insert" bisa dijalankan dua proses bersamaan dan menghasilkan duplikat. upsert() menyerahkannya ke database sebagai satu operasi atomik — asalkan unique index-nya benar-benar ada.

Query builder atau Eloquent?

KebutuhanPakaiKenapa
CRUD satu entitasEloquentRelasi, cast, event, policy ikut bekerja
Laporan dan agregasiQuery builderTidak butuh objek model, jauh lebih ringan
Update jutaan barisQuery builderSatu query, bukan sejuta objek
Ada logika bisnis di modelEloquentQuery builder melewati semua event
Migrasi & skrip pemeliharaanQuery builderTidak terikat bentuk model saat ini

Konsekuensi yang gampang terlewat. DB::table('produk')->delete() tidak menjalankan event deleting, tidak menghormati soft delete, dan tidak menyentuh relasi. Kadang itu persis yang kamu mau; kadang itu bug yang baru ketahuan sebulan kemudian saat ada yang bertanya kenapa berkas terkait tidak ikut terhapus.

Mengintip SQL

$q = DB::table('produk')->where('stok', '>', 0);

$q->toSql();          // "select * from `produk` where `stok` > ?"
$q->toRawSql();       // lengkap dengan nilainya — untuk ditempel ke klien SQL
$q->dd();             // cetak lalu hentikan
$q->explain()->dd();  // rencana eksekusi — apakah index-nya terpakai?

Latihan: tulis satu query laporan yang menghasilkan omzet per bulan selama setahun terakhir, jalankan ->explain()->dd(), dan periksa apakah database memindai seluruh tabel. Lalu tambahkan index pada created_at dan jalankan ulang explain-nya.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.