BAB 18: Query Builder

Membuat query database yang fluent dan aman tanpa menulis SQL mentah menggunakan Query Builder Laravel.

Migrasi yang kita buat di bab sebelumnya sudah membentuk struktur tabel catatan di database. Sekarang pertanyaannya bergeser: bagaimana cara membaca, menulis, dan memanipulasi data di tabel tersebut tanpa harus menulis SQL mentah yang panjang dan rawan typo?

Query Builder adalah jawaban pertama Laravel untuk masalah ini. Ia adalah lapisan di atas DB facade yang memberikan antarmuka berantai (fluent interface) — kamu merangkai method satu per satu, dan Laravel menerjemahkannya menjadi SQL yang benar dan aman. Semua nilai yang dimasukkan melalui Query Builder otomatis melewati parameter binding, sehingga SQL injection bukan lagi ancaman yang perlu dikhawatirkan secara manual.

Memulai Query

Setiap query dimulai dengan DB::table() yang menentukan tabel mana yang akan dioperasikan:

use Illuminate\Support\Facades\DB;

// Ambil semua catatan
$semuaCatatan = DB::table('catatan')->get();

// Ambil baris pertama saja
$satucatatan = DB::table('catatan')->first();

// Ambil berdasarkan primary key
$catatan = DB::table('catatan')->find(5);

// Ambil nilai satu kolom saja
$judul = DB::table('catatan')->where('id', 5)->value('judul');

get() mengembalikan Collection berisi objek stdClass untuk setiap baris. first() mengembalikan satu objek stdClass atau null jika tidak ada. Jika ingin melempar exception saat tidak ditemukan, gunakan firstOrFail().

Mengambil Kolom Tertentu

// Hanya ambil dua kolom
$catatan = DB::table('catatan')
    ->select('id', 'judul', 'prioritas')
    ->get();

// Ambil nilai satu kolom sebagai array flat
$daftarJudul = DB::table('catatan')
    ->pluck('judul');

// Dengan key dari kolom lain — menghasilkan ['id' => 'judul']
$peta = DB::table('catatan')
    ->pluck('judul', 'id');

pluck() sangat berguna untuk mengisi dropdown atau keperluan lain yang hanya butuh satu kolom tanpa overhead memuat semua data.

Filtering dengan Where

Kondisi WHERE adalah inti dari hampir semua query yang berguna. Query Builder menyediakan berbagai cara untuk mengekspresikannya:

// Kondisi dasar — operator default adalah '='
$catatan = DB::table('catatan')
    ->where('prioritas', 'tinggi')
    ->get();

// Dengan operator eksplisit
$catatan = DB::table('catatan')
    ->where('created_at', '>=', '2026-01-01')
    ->get();

// Beberapa kondisi AND
$catatan = DB::table('catatan')
    ->where('user_id', 1)
    ->where('selesai', false)
    ->where('prioritas', 'tinggi')
    ->get();

// Kondisi OR
$catatan = DB::table('catatan')
    ->where('prioritas', 'tinggi')
    ->orWhere('prioritas', 'mendesak')
    ->get();

Pengelompokan Kondisi

Ketika logika kondisi lebih kompleks — misalnya (A AND B) OR (C AND D) — gunakan closure untuk mengelompokkan:

// Cari catatan yang: (user_id = 1 DAN belum selesai) ATAU (prioritas = mendesak)
$catatan = DB::table('catatan')
    ->where(function ($query) {
        $query->where('user_id', 1)
              ->where('selesai', false);
    })
    ->orWhere('prioritas', 'mendesak')
    ->get();

Helper Where yang Sering Dipakai

// Nilai dalam rentang
$catatan = DB::table('catatan')
    ->whereBetween('created_at', ['2026-01-01', '2026-03-31'])
    ->get();

// Nilai dalam daftar
$catatan = DB::table('catatan')
    ->whereIn('prioritas', ['tinggi', 'mendesak'])
    ->get();

// Nilai null
$catatan = DB::table('catatan')
    ->whereNull('deleted_at')
    ->get();

// Pattern matching (LIKE)
$catatan = DB::table('catatan')
    ->whereLike('judul', '%belajar%')
    ->get();

// Filter berdasarkan tanggal
$catatan = DB::table('catatan')
    ->whereDate('created_at', '2026-03-17')
    ->get();

$catatan = DB::table('catatan')
    ->whereYear('created_at', 2026)
    ->whereMonth('created_at', 3)
    ->get();

Pengurutan dan Pembatasan Hasil

// Urutkan ascending
$catatan = DB::table('catatan')
    ->orderBy('judul', 'asc')
    ->get();

// Urutkan descending
$catatan = DB::table('catatan')
    ->orderByDesc('created_at')
    ->get();

// Shorthand untuk order berdasarkan created_at
$terbaru = DB::table('catatan')->latest()->get();
$terlama = DB::table('catatan')->oldest()->get();

// Batasi dan lewati
$catatan = DB::table('catatan')
    ->orderByDesc('created_at')
    ->limit(10)
    ->offset(20)      // lewati 20 baris pertama
    ->get();

Kombinasi limit dan offset adalah dasar paginasi manual. Meskipun untuk paginasi di aplikasi nyata kamu akan menggunakan Eloquent yang punya metode paginate(), memahami mekanisme ini di Query Builder tetap penting.

Agregasi

Fungsi agregat memungkinkan kamu menghitung ringkasan dari data tanpa mengambil semua baris:

$total        = DB::table('catatan')->count();
$totalUser    = DB::table('catatan')->where('user_id', 1)->count();
$totalSelesai = DB::table('catatan')->where('selesai', true)->count();

// Cek keberadaan — lebih efisien dari count() > 0
$adaCatatan = DB::table('catatan')->where('user_id', 1)->exists();
$tidakAda   = DB::table('catatan')->where('user_id', 999)->doesntExist();

exists() lebih efisien daripada count() > 0 karena database hanya perlu menemukan satu baris, bukan menghitung semua baris yang cocok.

Join Antar Tabel

Ketika data yang dibutuhkan tersebar di lebih dari satu tabel, gunakan join:

// Inner join — hanya catatan yang punya user
$catatan = DB::table('catatan')
    ->join('users', 'catatan.user_id', '=', 'users.id')
    ->select('catatan.id', 'catatan.judul', 'users.name as nama_penulis')
    ->get();

// Left join — semua catatan, user bisa null
$catatan = DB::table('catatan')
    ->leftJoin('users', 'catatan.user_id', '=', 'users.id')
    ->select('catatan.judul', 'users.name as nama_penulis')
    ->get();

Selalu gunakan select() secara eksplisit saat melakukan join untuk menghindari ambiguitas nama kolom. Tanpa itu, kolom id dari dua tabel yang di-join akan saling menimpa.

Insert, Update, Delete

Query Builder juga menangani operasi write:

Insert

// Insert satu baris
DB::table('catatan')->insert([
    'user_id'   => 1,
    'judul'     => 'Catatan baru',
    'isi'       => 'Isi catatan ini',
    'prioritas' => 'normal',
    'selesai'   => false,
    'created_at' => now(),
    'updated_at' => now(),
]);

// Insert dan dapatkan ID yang baru dibuat
$id = DB::table('catatan')->insertGetId([
    'user_id'    => 1,
    'judul'      => 'Catatan penting',
    'prioritas'  => 'tinggi',
    'selesai'    => false,
    'created_at' => now(),
    'updated_at' => now(),
]);

// Insert banyak baris sekaligus
DB::table('catatan')->insert([
    ['user_id' => 1, 'judul' => 'Catatan A', 'selesai' => false, 'created_at' => now(), 'updated_at' => now()],
    ['user_id' => 1, 'judul' => 'Catatan B', 'selesai' => false, 'created_at' => now(), 'updated_at' => now()],
    ['user_id' => 2, 'judul' => 'Catatan C', 'selesai' => false, 'created_at' => now(), 'updated_at' => now()],
]);

Update

// Update kolom tertentu
DB::table('catatan')
    ->where('id', 5)
    ->update([
        'selesai'    => true,
        'prioritas'  => 'rendah',
        'updated_at' => now(),
    ]);

// Increment dan decrement
DB::table('catatan')->where('id', 5)->increment('views');
DB::table('catatan')->where('id', 5)->increment('views', 5);
DB::table('catatan')->where('id', 5)->decrement('views');

// Upsert — insert jika belum ada, update jika sudah
DB::table('catatan')->upsert(
    [
        ['user_id' => 1, 'judul' => 'Catatan A', 'prioritas' => 'tinggi', 'selesai' => false],
    ],
    ['user_id', 'judul'],      // kolom untuk mendeteksi duplikat
    ['prioritas', 'selesai']   // kolom yang diupdate jika sudah ada
);

Delete

// Hapus satu baris
DB::table('catatan')->where('id', 5)->delete();

// Hapus beberapa baris
DB::table('catatan')
    ->where('user_id', 1)
    ->where('selesai', true)
    ->delete();

Query delete() tanpa klausa where() akan menghapus semua baris di tabel. Selalu pastikan ada kondisi yang tepat sebelum mengeksekusi delete.

Memproses Data Besar dengan Chunking

Ketika harus memproses ribuan atau jutaan baris, memuat semuanya ke memori sekaligus bisa menyebabkan aplikasi kehabisan RAM. Gunakan chunk() untuk memprosesnya dalam potongan-potongan kecil:

// Proses 200 baris per iterasi
DB::table('catatan')->orderBy('id')->chunk(200, function ($catatan) {
    foreach ($catatan as $item) {
        // proses setiap baris
    }
});

// Hentikan chunking lebih awal dengan return false
DB::table('catatan')->orderBy('id')->chunk(200, function ($catatan) {
    foreach ($catatan as $item) {
        if ($item->prioritas === 'mendesak') {
            return false; // berhenti
        }
    }
});

Untuk kasus yang lebih sederhana, lazy() mengembalikan LazyCollection yang mengambil baris satu per satu menggunakan PHP generator:

DB::table('catatan')
    ->orderBy('id')
    ->lazy()
    ->each(function ($item) {
        // proses setiap baris, satu per satu di memori
    });

Query Bersyarat

Sering kali filter query bergantung pada kondisi yang baru diketahui saat runtime — misalnya parameter pencarian yang mungkin ada atau tidak. Gunakan when() untuk menghindari if-else yang mengotori kode:

// Tanpa when() — cara yang kurang bersih
$query = DB::table('catatan')->where('user_id', $userId);
if ($request->has('prioritas')) {
    $query->where('prioritas', $request->prioritas);
}
if ($request->has('selesai')) {
    $query->where('selesai', $request->boolean('selesai'));
}
$catatan = $query->get();

// Dengan when() — lebih bersih dan mudah dibaca
$catatan = DB::table('catatan')
    ->where('user_id', $userId)
    ->when($request->has('prioritas'), function ($query) use ($request) {
        $query->where('prioritas', $request->prioritas);
    })
    ->when($request->has('selesai'), function ($query) use ($request) {
        $query->where('selesai', $request->boolean('selesai'));
    })
    ->get();

Debugging Query

Saat query tidak menghasilkan data yang diharapkan, lihat SQL yang sebenarnya dieksekusi:

// Dump SQL dan berhenti
DB::table('catatan')
    ->where('user_id', 1)
    ->where('prioritas', 'tinggi')
    ->ddRawSql();

// Dump SQL tapi lanjutkan eksekusi
DB::table('catatan')
    ->where('user_id', 1)
    ->dumpRawSql()
    ->get();

ddRawSql() menampilkan SQL dengan nilai binding yang sudah diisi — jauh lebih mudah dibaca daripada SQL dengan tanda tanya.

Latihan

Coba kerjakan latihan berikut dengan menggunakan Tinker (php artisan tinker) sebagai sandbox:

  1. Query dengan filter ganda — Ambil semua catatan milik user dengan id 1 yang belum selesai, diurutkan berdasarkan created_at terbaru, dan batasi hasilnya hanya 5 baris. Cetak jumlah baris yang dikembalikan.

  2. Agregasi per grup — Tulis query yang menghitung jumlah catatan per nilai prioritas (normal, tinggi, mendesak). Petunjuk: gunakan groupBy('prioritas') diikuti select yang menyertakan DB::raw('COUNT(*) as jumlah').

  3. Upsert data — Buat query upsert() yang mencoba menyisipkan catatan baru; jika catatan dengan user_id dan judul yang sama sudah ada, perbarui kolom prioritas dan updated_at-nya. Jalankan dua kali untuk memverifikasi bahwa eksekusi kedua memperbarui bukan menyisipkan baris baru.

Penutup Bab

Query Builder memberikan kenyamanan yang signifikan dibanding raw SQL — query lebih terbaca, aman dari injection, dan mudah dikondisikan. Tapi ia masih bekerja di level tabel dan kolom mentah; setiap hasil query adalah stdClass biasa tanpa logika domain apapun.

Untuk lapisan yang lebih tinggi lagi — di mana setiap baris tabel direpresentasikan sebagai objek PHP yang punya method, event, dan relasi — itulah yang disediakan Eloquent ORM. Eloquent menggunakan Query Builder di bawahnya, tapi menambahkan model, relasi, dan konvensi yang membuat kode lebih ekspresif dan lebih mudah dikelola seiring aplikasi tumbuh.

Referensi

  1. 1Database: Query Builder — Laravel 12.x Documentation
  2. 2Collections — Laravel 12.x Documentation