Query yang berhasil dieksekusi menyimpan hasilnya di dalam objek PDOStatement, dan tugas kita adalah mengambil baris demi baris dalam bentuk yang paling nyaman diproses. PDO menyediakan banyak fetch mode untuk hal ini: array asosiatif, array numerik, object anonim, hingga instance dari class buatan sendiri. Pada chapter ini kita akan membedah method fetch(), fetchAll(), dan fetchColumn(), lalu berkenalan dengan pola fetchAll() tingkat lanjut yang menghasilkan struktur data siap pakai. Seluruh contoh memakai SQLite in-memory dengan tabel produk yang sama sehingga mudah dibandingkan antar contoh.

Mode Fetch Dasar #

Fetch mode menentukan wujud setiap baris hasil. Mode ditentukan lewat konstanta PDO::FETCH_*, baik saat memanggil method fetch maupun lewat atribut default koneksi. Berikut mode yang paling sering dipakai:

ModeWujud setiap baris
PDO::FETCH_ASSOCarray asosiatif dengan nama kolom sebagai kunci
PDO::FETCH_NUMarray berindeks angka mulai dari 0
PDO::FETCH_BOTHgabungan kunci nama kolom dan angka (bawaan PDO)
PDO::FETCH_OBJobject anonim dengan properti bernama kolom
PDO::FETCH_CLASSinstance dari class yang kita tentukan
PDO::FETCH_LAZYobject dibuat malas saat properti pertama diakses

Mari kita lihat perbedaan wujudnya secara langsung:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

print_r($pdo->query("SELECT nama, stok FROM produk WHERE id = 1")->fetch(PDO::FETCH_NUM));
print_r($pdo->query("SELECT nama, stok FROM produk WHERE id = 1")->fetch(PDO::FETCH_ASSOC));

$row = $pdo->query("SELECT nama, stok FROM produk WHERE id = 1")->fetch(PDO::FETCH_OBJ);
echo "{$row->nama}: {$row->stok} pcs\n";

Output yang Dihasilkan:

text
Array
(
    [0] => Kopi Hitam
    [1] => 20
)
Array
(
    [nama] => Kopi Hitam
    [stok] => 20
)
Kopi Hitam: 20 pcs

Tiga pemanggilan query() di atas sengaja dibuat terpisah karena setiap PDOStatement memiliki kursor miliknya sendiri: satu panggilan fetch() memindahkan kursor satu baris, dan kursor tidak bisa dibagikan antar statement. Mode PDO::FETCH_LAZY berperilaku mirip FETCH_OBJ tetapi menunda pembuatan object sampai propertinya benar-benar diakses — berguna saat Anda hanya membaca sebagian kecil kolom dari baris yang banyak.

Iterasi Kursor dengan fetch #

Untuk memproses banyak baris satu per satu, pola while bersama fetch() adalah standarnya. Ketika kursor sudah berada di akhir hasil, fetch() mengembalikan false sehingga loop berhenti secara alami:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$stmt = $pdo->query("SELECT nama, stok FROM produk ORDER BY id");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo str_pad($row['nama'], 12) . " stok: {$row['stok']}\n";
}

echo "fetch setelah kursor habis: " . gettype($stmt->fetch()) . "\n";

Output yang Dihasilkan:

text
Kopi Hitam   stok: 20
Teh Melati   stok: 15
Roti Bakar   stok: 8
Air Mineral  stok: 50
fetch setelah kursor habis: boolean

Kondisi while ($row = $stmt->fetch(...)) aman dari jebakan nilai falsy karena hasil FETCH_ASSOC selalu berupa array — array berisi kolom tidak pernah bernilai false. Baris terbaru diproses segera setelah tiba, sehingga memori hanya menampung satu baris pada satu waktu. Pada driver seperti MySQL dengan unbuffered mode, pola ini bahkan memungkinkan pemrosesan hasil ketika server masih mengirim sisanya.

fetchAll versus fetch: Memori dan Kenyamanan #

fetchAll() mengambil seluruh baris sekaligus menjadi satu array besar, lalu mengosongkan statement. Pilihan antara keduanya adalah pertukaran antara kenyamanan dan memori:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$semua = $pdo->query("SELECT nama, stok FROM produk")->fetchAll(PDO::FETCH_ASSOC);
echo "fetchAll memuat " . count($semua) . " baris sekaligus\n";

$stmt = $pdo->query("SELECT nama, stok FROM produk");
$jumlah = 0;
while ($stmt->fetch(PDO::FETCH_ASSOC) !== false) {
    $jumlah++;
}
echo "iterasi kursor memproses $jumlah baris satu per satu\n";

Output yang Dihasilkan:

text
fetchAll memuat 4 baris sekaligus
iterasi kursor memproses 4 baris satu per satu

Gunakan fetchAll() ketika Anda memang butuh seluruh hasil sekaligus — misalnya untuk dihitung, diurutkan ulang, atau dikirim sebagai JSON — dan gunakan iterasi kursor ketika baris cukup diproses sekali lalu dibuang. Untuk hasil berukuran besar (puluhan ribu baris ke atas), fetchAll() menggandakan kebutuhan memori karena seluruh data menumpuk di RAM sebelum diproses.

Mengambil Satu Kolom dengan fetchColumn #

fetchColumn() mengembalikan nilai kolom pertama dari baris berikutnya langsung sebagai skalar — tanpa pembungkus array. Method ini paling pas untuk agregasi seperti COUNT(*):

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

echo "jumlah produk: " . $pdo->query("SELECT COUNT(*) FROM produk")->fetchColumn() . "\n";

$stmt = $pdo->query("SELECT nama FROM produk ORDER BY stok DESC LIMIT 2");
while (($nama = $stmt->fetchColumn()) !== false) {
    echo "stok terbanyak: $nama\n";
}

Output yang Dihasilkan:

text
jumlah produk: 4
stok terbanyak: Air Mineral
stok terbanyak: Kopi Hitam

Pemetaan ke Class dengan fetchObject #

Ketika aplikasi membesar, baris hasil sering lebih berguna sebagai object bertype daripada array polos. fetchObject() membuat instance dari class yang kita tentukan lalu mengisi propertinya sesuai nama kolom:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

class Produk
{
    public string $nama;
    public string $kategori;
    public int $stok;

    public function label(): string
    {
        return "{$this->nama} [{$this->kategori}] - stok {$this->stok}";
    }
}

$stmt = $pdo->query("SELECT nama, kategori, stok FROM produk ORDER BY id");
while ($produk = $stmt->fetchObject('Produk')) {
    echo $produk->label(), "\n";
}

$utama = $pdo->query(
    "SELECT nama, kategori, stok FROM produk WHERE kategori = 'utama'",
    PDO::FETCH_CLASS,
    'Produk'
)->fetchAll();
echo "kategori utama: " . count($utama) . " produk\n";
echo $utama[0]->label(), "\n";

Output yang Dihasilkan:

text
Kopi Hitam [utama] - stok 20
Teh Melati [utama] - stok 15
Roti Bakar [tambahan] - stok 8
Air Mineral [tambahan] - stok 50
kategori utama: 2 produk
Kopi Hitam [utama] - stok 20

Ada dua cara pada contoh di atas: fetchObject('Produk') per baris, atau mode PDO::FETCH_CLASS yang diberikan ke query()/fetchAll() sekaligus. Keduanya menghasilkan object Produk yang benar-benar bisa memanggil method seperti label() — perhatikan bahwa nilai stok dari SQLite yang tipenya longgar tetap terisi rapi ke property bertipe int.

Pola fetchAll untuk Struktur Data Siap Pakai #

Tiga kombinasi mode menghasilkan struktur yang biasanya harus kita rakit manual dengan loop. Pertama, PDO::FETCH_KEY_PAIR membuat array pasangan kunci-nilai dari query dua kolom:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$stokPerProduk = $pdo->query("SELECT nama, stok FROM produk")->fetchAll(PDO::FETCH_KEY_PAIR);
echo "stok Teh Melati: {$stokPerProduk['Teh Melati']}\n";

Output yang Dihasilkan:

text
stok Teh Melati: 15

Kedua, PDO::FETCH_UNIQUE memakai kolom pertama sebagai kunci array dan sisanya sebagai nilai — cocok untuk tabel pencarian (lookup) berdasarkan nama atau id:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$perNama = $pdo->query("SELECT nama, kategori, stok FROM produk")
    ->fetchAll(PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC);
echo "kategori Kopi Hitam: {$perNama['Kopi Hitam']['kategori']}\n";

Output yang Dihasilkan:

text
kategori Kopi Hitam: utama

Ketiga, PDO::FETCH_GROUP mengelompokkan baris-baris ke dalam list berdasarkan kolom pertama — misalnya daftar produk per kategori:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$perKategori = $pdo->query("SELECT kategori, nama, stok FROM produk ORDER BY id")
    ->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
print_r($perKategori);

Output yang Dihasilkan:

text
Array
(
    [utama] => Array
        (
            [0] => Array
                (
                    [nama] => Kopi Hitam
                    [stok] => 20
                )

            [1] => Array
                (
                    [nama] => Teh Melati
                    [stok] => 15
                )

        )

    [tambahan] => Array
        (
            [0] => Array
                (
                    [nama] => Roti Bakar
                    [stok] => 8
                )

            [1] => Array
                (
                    [nama] => Air Mineral
                    [stok] => 50
                )

        )

)

Perlu diketahui, tanpa kombinasi | PDO::FETCH_ASSOC, hasil FETCH_UNIQUE dan FETCH_GROUP memuat kunci ganda (nama kolom sekaligus angka) karena mengikuti mode bawaan PDO::FETCH_BOTH. Ketika nilai kolom pertama duplikat, FETCH_UNIQUE menyimpan baris terakhir, sedangkan FETCH_GROUP menampung semua baris ke dalam list milik kunci yang sama.

Membaca Informasi Kolom #

Kadang kode perlu tahu bentuk hasil sebelum data diproses, misalnya untuk membuat tabel HTML generik. columnCount() melaporkan jumlah kolom hasil, dan getColumnMeta() memberi metadata per kolom:

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE produk (id INTEGER PRIMARY KEY, nama TEXT, kategori TEXT, stok INTEGER)");
$pdo->exec("INSERT INTO produk (nama, kategori, stok) VALUES
    ('Kopi Hitam', 'utama', 20),
    ('Teh Melati', 'utama', 15),
    ('Roti Bakar', 'tambahan', 8),
    ('Air Mineral', 'tambahan', 50)");

$stmt = $pdo->query("SELECT nama, stok FROM produk LIMIT 1");
echo "jumlah kolom: " . $stmt->columnCount() . "\n";

Output yang Dihasilkan:

text
jumlah kolom: 2

Metadata per kolom tersedia lewat getColumnMeta($index) yang mengembalikan array berisi, antara lain, name (label kolom), native_type (tipe asli menurut driver), dan len (panjang):

php
$stmt = $pdo->query("SELECT nama, stok FROM produk LIMIT 1");
$meta = $stmt->getColumnMeta(0);
echo $meta['name'] . " bertipe " . $meta['native_type'] . "\n";

Isi getColumnMeta() berbeda-beda antar driver, dan dokumentasi php.net menandainya sebagai eksperimental, sehingga cukup andal untuk laporan di skrip CLI sederhana namun sebaiknya tidak menjadi fondasi logika bisnis.

Ringkasan #

fetch() mengiterasi kursor hemat memori, fetchAll() memuat semuanya sekaligus, dan fetchColumn() mengambil skalar tunggal dengan perbandingan ketat !== false. Mode seperti FETCH_CLASS, FETCH_KEY_PAIR, FETCH_UNIQUE, dan FETCH_GROUP mengubah baris mentah menjadi struktur yang langsung siap dipakai aplikasi. Namun mengambil data hanyalah setengah cerita: ketika beberapa query harus berhasil bersama-sama atau gagal bersama-sama, kita membutuhkan transaksi — topik chapter berikutnya.

Referensi #