Belajar CodeIgniter - Advanced Query & Eloquent Alternatives
Episode 21 of 27

Belajar CodeIgniter - Advanced Query & Eloquent Alternatives

Memperdalam Query Builder: join, subquery, aggregate, dan union, lalu mengelola data konsisten dengan transaksi database serta memanfaatkan model callbacks untuk logika otomatis pada setiap operasi.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Di episode 6-7 kita memakai Query Builder untuk operasi dasar dan model untuk CRUD. Aplikasi nyata tidak berhenti di situ: laporan butuh aggregate, daftar butuh join antar tabel, dan transaksi keuangan menuntut operasi yang tidak boleh setengah jadi. Di episode 21 ini kita memperdalam semua itu.

Mengapa penting? Karena data aplikasi saling berelasi — post punya author, order punya item, kategori punya banyak post. Kemampuan menggabungkan data (join), merangkum (aggregate), dan menjaga konsistensi (transaction) adalah yang membedakan aplikasi "demo" dari aplikasi produksi yang bisa dipercaya.

Join Antar Tabel

Query Builder menyediakan method join(). Ambil post beserta nama penulisnya:

Join posts dengan users
$db = \Config\Database::connect();
 
$posts = $db->table('posts')
    ->select('posts.id, posts.title, posts.status, users.name AS author')
    ->join('users', 'users.id = posts.user_id', 'left')
    ->orderBy('posts.created_at', 'DESC')
    ->get()
    ->getResultArray();
 
// [
//   ['id' => 1, 'title' => 'Hello', 'status' => 'published', 'author' => 'Admin'],
//   ...
// ]

Tipe join: left (semua baris kiri, kanan boleh kosong), inner (hanya yang cocok), right. Aturan praktis: gunakan left saat kolom kiri (misal posts) harus selalu tampil meskipun relasi tidak ada.

Subquery

Subquery = query di dalam query. Contoh: post yang jumlah komentarnya di atas rata-rata.

Subquery dengan getCompiledSelect
$sub = $db->table('comments')
    ->select('post_id, COUNT(*) AS total')
    ->groupBy('post_id')
    ->getCompiledSelect();
 
$posts = $db->table('posts')
    ->select('posts.id, posts.title, c.total')
    ->join("({$sub}) c", 'c.post_id = posts.id', 'left')
    ->orderBy('c.total', 'DESC')
    ->get()
    ->getResultArray();

getCompiledSelect() mengambil SQL yang terbentuk tanpa menjalankannya — lalu kita pakai sebagai subquery dalam join.

Tip

Mulailah dari query yang benar di database dulu (via php spark db:query), baru terjemahkan ke Query Builder. Query Builder menyembunyikan kompleksitas SQL, tapi kalian tetap harus tahu SQL yang ingin dihasilkan agar tidak menebak-nebak.

Aggregate dan Group By

Untuk laporan, Query Builder menyediakan method aggregate:

Aggregate per kategori
$stats = $db->table('posts')
    ->select('categories.name AS kategori, COUNT(posts.id) AS jumlah')
    ->join('categories', 'categories.id = posts.category_id')
    ->groupBy('categories.id')
    ->get()
    ->getResultArray();
 
// [['kategori' => 'Tutorial', 'jumlah' => 12], ...]

Method aggregate lain: selectAvg(), selectSum(), selectMax(), selectMin(), selectCount(). Tambahkan having() untuk memfilter hasil aggregate:

Filter hasil aggregate
$db->table('posts')
    ->select('categories.name, COUNT(posts.id) AS jumlah')
    ->join('categories', 'categories.id = posts.category_id')
    ->groupBy('categories.id')
    ->having('jumlah > 5')
    ->get();

Perbedaan where vs having: where memfilter baris sebelum di-group, having memfilter hasil setelah di-group.

Transaksi Database

Transaksi menjamin sekumpulan operasi berjalan all-or-nothing: jika satu gagal, semua dibatalkan. Bayangkan memindahkan uang: mengurangi saldo pengirim dan menambah saldo penerima harus terjadi bersama-sama.

Transaksi dengan transStart/transComplete
$db = \Config\Database::connect();
 
$db->transStart();
 
$db->table('accounts')->where('id', $fromId)
    ->set('balance', 'balance - 100000', false)
    ->update();
 
$db->table('accounts')->where('id', $toId)
    ->set('balance', 'balance + 100000', false)
    ->update();
 
$db->transComplete();
 
if ($db->transStatus() === false) {
    log_message('error', 'Transfer gagal: dari #{from} ke #{to}', [
        'from' => $fromId,
        'to'   => $toId,
    ]);
    return redirect()->back()->with('error', 'Transaksi gagal, tidak ada perubahan.');
}

transStart() memulai, transComplete() menutup dan melakukan commit jika semua berhasil. transStatus() memberi tahu hasilnya.

Ada dua mode:

  • Transaksi sederhana (transStart/transComplete): jika query gagal, rollback otomatis dan transStatus() false.
  • Transaksi ketat (transBegin/transCommit/transRollback): commit/rollback manual, dan jika ingin error dilempar:
Transaksi ketat
$db = \Config\Database::connect();
$db->transBegin();
 
try {
    $db->table('orders')->insert($order);
    $db->table('order_items')->insertBatch($items);
    $db->transCommit();
} catch (\Throwable $e) {
    $db->transRollback();
    log_message('error', 'Order gagal: {msg}', ['msg' => $e->getMessage()]);
}

Warning

Operasi yang melibatkan banyak tabel yang saling bergantung (order + item, transfer + log) wajib transaksi. Tanpa transaksi, kegagalan di tengah operasi meninggalkan data setengah jadi — order tanpa item, atau saldo yang terpotong tapi tidak sampai tujuan.

Model Callbacks (Events)

CodeIgniter\Model menyediakan callbacks — method yang dipanggil otomatis sebelum/sesudah operasi. Contoh paling berguna: hash password sebelum insert, atau set slug otomatis.

app/Models/UserModel.php
<?php
 
namespace App\Models;
 
use CodeIgniter\Model;
 
class UserModel extends Model
{
    protected $table         = 'users';
    protected $allowedFields = ['name', 'email', 'password', 'role'];
    protected $useTimestamps = true;
    protected $beforeInsert  = ['hashPassword'];
 
    protected function hashPassword(array $data): array
    {
        if (isset($data['data']['password'])) {
            $data['data']['password'] = password_hash($data['data']['password'], PASSWORD_DEFAULT);
        }
 
        return $data;
    }
}

Callback yang tersedia: $beforeInsert, $afterInsert, $beforeUpdate, $afterUpdate, $beforeFind, $afterFind, $beforeDelete, $afterDelete. Setiap callback menerima dan mengembalikan array $data — jadi kalian bisa memodifikasi data sebelum diproses.

Praktik slug otomatis di callback
protected $beforeInsert = ['setSlug'];
 
protected function setSlug(array $data): array
{
    if (isset($data['data']['title']) && empty($data['data']['slug'])) {
        $data['data']['slug'] = (new \App\Libraries\Slugger())->make($data['data']['title']);
    }
 
    return $data;
}

Logika bisnis (hash password, slug, timestamp) terkumpul di model, bukan berserakan di controller — inilah cara model menjadi "Eloquent-like" di CodeIgniter: tempat logika data tinggal.

Note

Callbacks model adalah kunci untuk meniru pola ORM modern (seperti Eloquent di Laravel) di CodeIgniter: observer events beforeInsert/afterInsert mirip konsep model events, tapi tetap ringan dan eksplisit. Ini filosofi CodeIgniter — kekuatan tanpa kompleksitas berlebihan.

Praktik: Laporan Kompleks + Transaksi

Gabungkan semuanya — laporan penjualan per kategori dengan transaksi untuk mencatat laporan:

Laporan + transaksi
$db = \Config\Database::connect();
 
$db->transStart();
 
$report = $db->table('orders')
    ->select('categories.name, SUM(order_items.price * order_items.qty) AS total')
    ->join('order_items', 'order_items.order_id = orders.id')
    ->join('products', 'products.id = order_items.product_id')
    ->join('categories', 'categories.id = products.category_id')
    ->where('orders.status', 'paid')
    ->groupBy('categories.id')
    ->get()
    ->getResultArray();
 
$db->table('report_logs')->insert([
    'generated_at' => date('Y-m-d H:i:s'),
    'rows'         => count($report),
]);
 
$db->transComplete();
 
if ($db->transStatus() === false) {
    log_message('error', 'Laporan gagal dibuat');
    return $this->fail('Terjadi kesalahan membuat laporan');
}
 
return $this->respond($report);

Ringkasan Advanced Query & Transaksi

  • join('table', 'condition', 'type') menggabungkan tabel; gunakan left untuk relasi opsional.
  • Subquery via getCompiledSelect(); aggregate via selectSum()/selectCount()/dll.
  • groupBy() + having() untuk laporan berkelompok.
  • Transaksi: transStart/transComplete/transStatus atau transBegin/transCommit/transRollback.
  • Model callbacks (beforeInsert, afterUpdate, dll) menampung logika otomatis di model.

Penutup

Inti yang harus dibawa pulang:

  • Join dan subquery menghubungkan data antar tabel.
  • Aggregate + groupBy + having untuk laporan.
  • Transaksi menjamin konsistensi data — wajib untuk operasi multi-tabel.
  • Callbacks model memusatkan logika data (hash, slug, timestamp).

Di episode 22 selanjutnya kita akan membahas queue, cron & background jobs — membuat command custom di spark, mengotomasi dengan cron, dan memahami konsep queue untuk pekerjaan berat yang tidak bisa ditangani di request. Aplikasi kalian mulai bekerja di balik layar!