MongoDB

MongoDB #

MongoDB adalah database NoSQL yang menyimpan data dalam format dokumen BSON (Binary JSON) — berbeda dari database relasional yang menyimpan data dalam tabel dengan baris dan kolom yang kaku, MongoDB menyimpan dokumen yang strukturnya bisa berbeda-beda dalam satu koleksi. Ini sangat berguna untuk data yang skemanya sering berubah, data yang secara alami bersifat hierarkis (produk dengan atribut yang beragam, log event, konten CMS), atau saat kamu butuh scale horizontal yang mudah. PHP mengakses MongoDB melalui library resmi mongodb/mongodb yang dibangun di atas ekstensi PECL mongodb. Artikel ini membahas semua aspek penting: CRUD, aggregation pipeline yang powerful, indexing, transaksi, dan kapan MongoDB tepat digunakan.

Kapan MongoDB Tepat #

flowchart TD
    A{Data memiliki\nskema tetap?} -- Ya --> B{Butuh JOIN\nkompleks?}
    B -- Ya --> C[Database Relasional\nMySQL / PostgreSQL]
    B -- Tidak --> D{Volume write\nsangat tinggi?}
    D -- Ya --> E[MongoDB\nAtau time-series DB]
    D -- Tidak --> C

    A -- Tidak --> F{Struktur data\nberubah-ubah?}
    F -- Ya --> G[MongoDB\nFleksibel skema]
    F -- Tidak --> H{Data hierarkis\natau dokumen?}
    H -- Ya --> G
    H -- Tidak --> C

    style C fill:#dbeafe
    style G fill:#dcfce7
Pilih MongoDB jika:
  ✓ Skema berubah sering — atribut produk yang beragam, konten CMS
  ✓ Data secara alami hierarkis — order dengan items tertanam
  ✓ Read/write volume tinggi dengan skalabilitas horizontal
  ✓ Logging, analytics, time-series, event store
  ✓ Prototyping cepat tanpa migrasi skema

Pilih database relasional jika:
  ✗ Data sangat terstruktur dan relasi kompleks
  ✗ Butuh transaksi ACID yang kuat antar banyak koleksi
  ✗ Laporan dan query analitik yang kompleks
  ✗ Tim lebih familiar dengan SQL

Instalasi #

# 1. Install ekstensi PHP mongodb via PECL
sudo apt install php8.3-dev php-pear
sudo pecl install mongodb

# Aktifkan ekstensi
echo "extension=mongodb.so" | sudo tee /etc/php/8.3/mods-available/mongodb.ini
sudo phpenmod mongodb

# 2. Install library PHP (abstraksi di atas ekstensi)
composer require mongodb/mongodb

# Verifikasi
php -r "echo extension_loaded('mongodb') ? 'OK' : 'Tidak ada'; echo PHP_EOL;"

Koneksi #

<?php
require 'vendor/autoload.php';

use MongoDB\Client;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;

// Koneksi dasar
$client = new Client('mongodb://localhost:27017');

// Dengan autentikasi
$client = new Client('mongodb://username:password@localhost:27017/authdb');

// Dengan opsi koneksi
$client = new Client('mongodb://localhost:27017', [
    'serverSelectionTimeoutMS' => 3000,
    'connectTimeoutMS'         => 5000,
    'socketTimeoutMS'          => 30000,
    'maxPoolSize'              => 10,
]);

// MongoDB Atlas (cloud)
$client = new Client(
    'mongodb+srv://username:[email protected]/?retryWrites=true&w=majority'
);

// Pilih database dan koleksi
$db      = $client->myapp;                      // database
$users   = $db->users;                           // koleksi users
$produk  = $client->myapp->produk;              // shorthand
$orders  = $client->selectCollection('myapp', 'orders'); // cara eksplisit

CRUD Operasi #

Create — Menyimpan Dokumen #

<?php
// insertOne — satu dokumen
$hasil = $users->insertOne([
    'nama'       => 'Budi Santoso',
    'email'      => '[email protected]',
    'umur'       => 28,
    'aktif'      => true,
    'tags'       => ['php', 'mongodb', 'backend'],
    'alamat'     => [
        'jalan'  => 'Jl. Sudirman No. 1',
        'kota'   => 'Jakarta',
        'kodePos'=> '10220',
    ],
    'created_at' => new UTCDateTime(), // waktu sekarang
]);

echo "ID baru: " . $hasil->getInsertedId() . "\n"; // ObjectId

// insertMany — banyak dokumen sekaligus
$hasilBanyak = $produk->insertMany([
    ['nama' => 'Laptop Pro',  'harga' => 15000000, 'stok' => 5,  'kategori' => 'elektronik'],
    ['nama' => 'Monitor 4K',  'harga' => 5000000,  'stok' => 12, 'kategori' => 'elektronik'],
    ['nama' => 'Mouse Ergo',  'harga' => 250000,   'stok' => 0,  'kategori' => 'aksesoris'],
    ['nama' => 'Keyboard Mech','harga' => 800000,   'stok' => 8,  'kategori' => 'aksesoris'],
]);

echo "Diinsert: " . $hasilBanyak->getInsertedCount() . " dokumen\n";
echo "IDs: " . implode(', ', array_map('strval', $hasilBanyak->getInsertedIds())) . "\n";

Read — Mencari Dokumen #

<?php
// findOne — satu dokumen
$user = $users->findOne(['email' => '[email protected]']);
if ($user !== null) {
    echo $user['nama'] . "\n"; // Budi Santoso
    echo $user['_id'] . "\n";  // ObjectId sebagai string
}

// Cari by ObjectId
$id   = new ObjectId('64f5a1b2c3d4e5f6a7b8c9d0');
$user = $users->findOne(['_id' => $id]);

// find — banyak dokumen dengan filter
$cursor = $produk->find(
    // Filter
    ['kategori' => 'elektronik', 'stok' => ['$gt' => 0]],
    // Opsi
    [
        'projection' => ['nama' => 1, 'harga' => 1, '_id' => 0], // pilih kolom
        'sort'       => ['harga' => 1],                           // ascending
        'limit'      => 10,
        'skip'       => 0,
    ]
);

foreach ($cursor as $doc) {
    echo "{$doc['nama']}: Rp " . number_format($doc['harga']) . "\n";
}

// Operator query MongoDB
$hasil = $produk->find([
    'harga'    => ['$gte' => 100000, '$lte' => 5000000], // rentang harga
    'stok'     => ['$gt' => 0],                           // stok > 0
    'kategori' => ['$in' => ['elektronik', 'aksesoris']], // salah satu dari
    'nama'     => ['$regex' => 'Laptop', '$options' => 'i'], // regex case-insensitive
]);

// $or — salah satu kondisi terpenuhi
$hasil = $users->find([
    '$or' => [
        ['role' => 'admin'],
        ['aktif' => true, 'umur' => ['$gte' => 18]],
    ]
]);

// countDocuments — hitung dokumen yang cocok
$total = $produk->countDocuments(['kategori' => 'elektronik', 'stok' => ['$gt' => 0]]);

// distinct — nilai unik dari satu field
$kategori = $produk->distinct('kategori');
// ['elektronik', 'aksesoris']

Update — Mengubah Dokumen #

<?php
// updateOne — perbarui satu dokumen
$hasil = $produk->updateOne(
    ['_id' => new ObjectId('...')],     // filter
    [
        '$set'   => ['harga' => 14000000, 'updated_at' => new UTCDateTime()],
        '$inc'   => ['stok' => -1],     // kurangi stok 1
        '$push'  => ['tags' => 'sale'], // tambah ke array
    ]
);

echo "Dicocokkan: " . $hasil->getMatchedCount() . "\n";
echo "Diubah: "     . $hasil->getModifiedCount() . "\n";

// updateMany — perbarui semua yang cocok
$produk->updateMany(
    ['kategori' => 'elektronik', 'stok' => ['$gt' => 0]],
    ['$mul' => ['harga' => 1.1]] // naikkan harga 10%
);

// findOneAndUpdate — cari, update, dan kembalikan dokumen
$dokumenLama = $produk->findOneAndUpdate(
    ['_id' => new ObjectId('...')],
    ['$set' => ['status' => 'sold_out']],
    ['returnDocument' => \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER]
    // RETURN_DOCUMENT_AFTER: kembalikan dokumen SETELAH update
    // RETURN_DOCUMENT_BEFORE: kembalikan dokumen SEBELUM update (default)
);

// upsert — update jika ada, insert jika tidak ada
$hasil = $produk->updateOne(
    ['sku' => 'LAP-001'],
    [
        '$set'      => ['nama' => 'Laptop Pro X', 'harga' => 16000000],
        '$setOnInsert' => ['created_at' => new UTCDateTime()], // hanya saat insert
    ],
    ['upsert' => true]
);

echo $hasil->isUpsert() ? "Dokumen baru dibuat\n" : "Dokumen diupdate\n";

// Operator update penting:
// $set    — set nilai field
// $unset  — hapus field
// $inc    — increment/decrement angka
// $mul    — kalikan nilai dengan faktor
// $push   — tambah elemen ke array
// $pull   — hapus elemen dari array
// $addToSet — tambah ke array hanya jika belum ada
// $rename — rename field
// $currentDate — set ke tanggal sekarang

Delete — Menghapus Dokumen #

<?php
// deleteOne
$hasil = $users->deleteOne(['email' => '[email protected]']);
echo "Dihapus: " . $hasil->getDeletedCount() . "\n";

// deleteMany
$hasil = $produk->deleteMany([
    'stok'       => 0,
    'updated_at' => ['$lt' => new UTCDateTime(strtotime('-30 days') * 1000)],
]);

// findOneAndDelete — cari, hapus, kembalikan dokumen yang dihapus
$terhapus = $orders->findOneAndDelete(['status' => 'cancelled', 'total' => ['$lt' => 10000]]);
if ($terhapus !== null) {
    echo "Hapus order: " . $terhapus['_id'] . "\n";
}

Aggregation Pipeline #

Aggregation pipeline adalah cara paling powerful menganalisis data di MongoDB — mirip dengan SQL’s GROUP BY, JOIN, dan HAVING dikombinasikan:

<?php
// Laporan penjualan per kategori
$pipeline = [
    // Stage 1: Filter dokumen
    ['$match' => ['status' => 'selesai', 'created_at' => ['$gte' => new UTCDateTime(strtotime('-30 days') * 1000)]]],

    // Stage 2: Lookup (JOIN) ke koleksi produk
    ['$lookup' => [
        'from'         => 'produk',    // koleksi sumber
        'localField'   => 'produk_id', // field di orders
        'foreignField' => '_id',        // field di produk
        'as'           => 'produk_info',
    ]],

    // Stage 3: Unwind array hasil lookup
    ['$unwind' => '$produk_info'],

    // Stage 4: Group dan agregasi
    ['$group' => [
        '_id'            => '$produk_info.kategori',
        'total_penjualan'=> ['$sum' => '$total'],
        'jumlah_order'   => ['$sum' => 1],
        'rata_rata'      => ['$avg' => '$total'],
        'terbesar'       => ['$max' => '$total'],
    ]],

    // Stage 5: Tambahkan field baru
    ['$addFields' => [
        'kategori' => '$_id',
    ]],

    // Stage 6: Buang field yang tidak perlu
    ['$project' => [
        '_id'             => 0,
        'kategori'        => 1,
        'total_penjualan' => 1,
        'jumlah_order'    => 1,
        'rata_rata'       => ['$round' => ['$rata_rata', 0]],
        'terbesar'        => 1,
    ]],

    // Stage 7: Urutkan
    ['$sort' => ['total_penjualan' => -1]],

    // Stage 8: Limit hasil
    ['$limit' => 10],
];

$hasil = $db->orders->aggregate($pipeline);

foreach ($hasil as $row) {
    printf(
        "%s: %d order, total Rp %s\n",
        $row['kategori'],
        $row['jumlah_order'],
        number_format($row['total_penjualan'])
    );
}

Indexing #

Index di MongoDB sangat penting untuk performa query. Tanpa index, MongoDB harus scan seluruh koleksi:

<?php
// Index tunggal
$users->createIndex(['email' => 1], ['unique' => true, 'name' => 'idx_email_unique']);

// Index compound
$produk->createIndex(
    ['kategori' => 1, 'harga' => 1], // ascending
    ['name' => 'idx_kategori_harga']
);

// Index descending
$orders->createIndex(['created_at' => -1], ['name' => 'idx_created_desc']);

// Text index untuk full-text search
$produk->createIndex(
    ['nama' => 'text', 'deskripsi' => 'text'],
    ['name' => 'idx_text_search', 'weights' => ['nama' => 10, 'deskripsi' => 1]]
);

// Gunakan text index
$hasil = $produk->find(['$text' => ['$search' => 'laptop gaming']]);

// TTL index — dokumen otomatis dihapus setelah waktu tertentu
$sessions->createIndex(
    ['expired_at' => 1],
    ['expireAfterSeconds' => 0, 'name' => 'idx_ttl_session']
);
// Dokumen dengan expired_at di masa lalu akan otomatis dihapus

// Partial index — hanya index dokumen yang memenuhi kondisi
$produk->createIndex(
    ['harga' => 1],
    ['partialFilterExpression' => ['stok' => ['$gt' => 0]], 'name' => 'idx_harga_tersedia']
);

// Lihat semua index
$indexes = iterator_to_array($produk->listIndexes());
foreach ($indexes as $index) {
    echo $index->getName() . ": " . json_encode($index->getKey()) . "\n";
}

// Hapus index
$produk->dropIndex('idx_kategori_harga');

Transaksi Multi-Dokumen #

MongoDB 4.0+ mendukung transaksi ACID untuk operasi yang melibatkan beberapa dokumen atau koleksi:

<?php
// Transaksi membutuhkan Replica Set atau Sharded Cluster
// Tidak bisa di standalone mongod

$session = $client->startSession();

try {
    $session->startTransaction([
        'readConcern'  => new \MongoDB\Driver\ReadConcern('snapshot'),
        'writeConcern' => new \MongoDB\Driver\WriteConcern(\MongoDB\Driver\WriteConcern::MAJORITY),
    ]);

    // Semua operasi dalam transaksi harus sertakan session
    $orders->insertOne(
        [
            'user_id'   => new ObjectId('...'),
            'items'     => [['produk_id' => new ObjectId('...'), 'qty' => 2]],
            'total'     => 30000000,
            'status'    => 'pending',
        ],
        ['session' => $session]
    );

    // Kurangi stok
    $produk->updateOne(
        ['_id' => new ObjectId('...')],
        ['$inc' => ['stok' => -2]],
        ['session' => $session]
    );

    $session->commitTransaction();
    echo "Transaksi berhasil\n";

} catch (\MongoDB\Driver\Exception\CommandException $e) {
    $session->abortTransaction();
    throw $e;
} finally {
    $session->endSession();
}

GridFS — Menyimpan File Besar #

GridFS adalah spesifikasi MongoDB untuk menyimpan file yang lebih besar dari batas ukuran dokumen (16MB):

<?php
use MongoDB\GridFS\Bucket;

$bucket = $db->selectGridFSBucket(['bucketName' => 'uploads']);

// Upload file
$stream   = fopen('/path/to/gambar.jpg', 'rb');
$fileId   = $bucket->uploadFromStream('gambar.jpg', $stream, [
    'metadata' => ['uploader' => 'Budi', 'kategori' => 'produk'],
]);
fclose($stream);
echo "File ID: $fileId\n";

// Download file
$outputStream = fopen('/tmp/download.jpg', 'wb');
$bucket->downloadToStream($fileId, $outputStream);
fclose($outputStream);

// Streaming ke browser
header('Content-Type: image/jpeg');
$downloadStream = $bucket->openDownloadStream($fileId);
fpassthru($downloadStream);

// Cari file
$cursor = $bucket->find(['metadata.kategori' => 'produk']);
foreach ($cursor as $fileDoc) {
    echo $fileDoc->filename . " (" . $fileDoc->length . " bytes)\n";
}

// Hapus file
$bucket->delete($fileId);

Anti-Pattern MongoDB yang Sering Ditemui #

<?php
// ✗ Anti-pattern 1: query tanpa index pada koleksi besar
$produk->find(['nama' => 'Laptop']); // full collection scan jika tidak ada index pada 'nama'
// ✓ Buat index: $produk->createIndex(['nama' => 1]);

// ✗ Anti-pattern 2: menyimpan dokumen besar dengan array yang tumbuh tak terbatas
$orders->updateOne(
    ['_id' => $orderId],
    ['$push' => ['log' => "Event $i"]] // array log bisa tidak terbatas!
);
// ✓ Simpan log di koleksi terpisah, bukan embed di dokumen

// ✗ Anti-pattern 3: pakai _id string yang tidak perlu
$produk->insertOne(['_id' => 'laptop-001', 'nama' => 'Laptop']); // boleh, tapi...
// Jika 'laptop-001' tidak unik di sistem kamu, pakai ObjectId yang dijamin unik

// ✗ Anti-pattern 4: join terlalu banyak koleksi di aggregation
// MongoDB bukan database relasional — jika butuh banyak $lookup, pertimbangkan:
// - Embed data yang sering diakses bersama
// - Gunakan database relasional jika relasi kompleks adalah kebutuhan utama

// ✗ Anti-pattern 5: tidak set timeout pada operasi
$cursor = $produk->find([], ['noCursorTimeout' => true]); // cursor tidak pernah timeout — bahaya!
// ✓ Biarkan default timeout aktif, atau set maxTimeMS
$cursor = $produk->find([], ['maxTimeMS' => 5000]); // timeout 5 detik

Ringkasan #

  • Library mongodb/mongodb adalah abstraksi resmi di atas ekstensi PECL — selalu gunakan library ini, bukan ekstensi langsung.
  • BSON tipe data — gunakan MongoDB\BSON\ObjectId untuk ID, MongoDB\BSON\UTCDateTime untuk tanggal (bukan PHP DateTime langsung), dan MongoDB\BSON\Decimal128 untuk angka desimal presisi tinggi.
  • Operator query: $gt/$gte/$lt/$lte (perbandingan), $in/$nin (dalam daftar), $regex (pattern), $or/$and/$not (logika), $exists (field ada/tidak).
  • Operator update: $set (set nilai), $unset (hapus field), $inc (increment), $push/$pull (array), $addToSet (tambah unik ke array).
  • Aggregation pipeline adalah cara paling powerful untuk analitik — gunakan $match dulu untuk filter sebelum $group, $lookup, dll. agar hanya data yang perlu yang diproses.
  • Index wajib untuk semua field yang sering di-query. Gunakan explain() untuk memverifikasi query menggunakan index. TTL index sangat berguna untuk data yang punya waktu kadaluarsa (session, cache, token).
  • Transaksi multi-dokumen tersedia di MongoDB 4.0+ tapi butuh Replica Set. Untuk standalone, atomisitas hanya dijamin di tingkat satu dokumen.
  • GridFS untuk file > 16MB — jangan simpan file besar langsung di field dokumen biasa.

← Sebelumnya: PostgreSQL   Berikutnya: Elasticsearch →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact