MongoDB

MongoDB #

MongoDB adalah database NoSQL berbasis dokumen yang menyimpan data dalam format BSON (Binary JSON) — dokumen yang fleksibel, tidak memerlukan schema yang tetap, dan bisa bersarang secara alami. Alih-alih tabel dengan baris dan kolom, MongoDB menggunakan koleksi (collection) berisi dokumen (document) yang masing-masing bisa punya struktur berbeda. Ini sangat cocok untuk data yang strukturnya berubah-ubah, katalog produk dengan atribut berbeda per kategori, log event, atau data hierarki yang alami. Di Kotlin, ada dua cara utama: MongoDB Driver for JVM (driver resmi, lengkap, tapi verbose) dan KMongo (wrapper Kotlin yang lebih idiomatis, dengan dukungan kotlinx.serialization). Artikel ini membahas keduanya secara mendalam.

Kapan Menggunakan MongoDB vs SQL #

Sebelum memilih MongoDB, pahami dulu kapan ia tepat:

PILIH MongoDB jika:
  ✓ Data punya struktur yang bervariasi (produk dengan atribut berbeda per kategori)
  ✓ Data hierarki alami yang sering diakses bersama (pesanan + item + alamat)
  ✓ Skema perlu berubah sering tanpa migrasi kompleks
  ✓ Volume tulis sangat tinggi dan butuh horizontal scaling
  ✓ Data geospasial atau time-series
  ✓ Prototyping cepat tanpa overhead skema database

PILIH SQL (PostgreSQL, MySQL) jika:
  ✓ Data relasional yang perlu JOIN kompleks antar entitas
  ✓ Transaksi ACID yang sangat ketat (keuangan, inventori)
  ✓ Laporan dan analitik kompleks (GROUP BY, window function, CTE)
  ✓ Tim sudah familiar dengan SQL
  ✓ Integritas referensial penting (foreign key)

Setup dan Dependensi #

// build.gradle.kts
dependencies {
    // MongoDB Driver for JVM (resmi)
    implementation("org.mongodb:mongodb-driver-sync:5.0.1")

    // KMongo — wrapper idiomatis Kotlin (opsional, alternatif driver resmi)
    implementation("org.litote.kmongo:kmongo:4.11.0")
    implementation("org.litote.kmongo:kmongo-serialization:4.11.0")

    // kotlinx.serialization untuk serialisasi data class
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}

// Tambahkan di plugins jika menggunakan KMongo dengan serialization
plugins {
    kotlin("plugin.serialization") version "2.0.0"
}

Koneksi ke MongoDB #

import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoDatabase
import com.mongodb.MongoClientSettings
import com.mongodb.ServerAddress
import com.mongodb.ConnectionString

object KoneksiMongo {
    private val client: MongoClient by lazy {
        // Koneksi sederhana
        MongoClients.create("mongodb://localhost:27017")

        // Atau koneksi dengan semua opsi
        // MongoClients.create(
        //     MongoClientSettings.builder()
        //         .applyConnectionString(ConnectionString(
        //             "mongodb://user:password@localhost:27017/myapp?authSource=admin"
        //         ))
        //         .applyToConnectionPoolSettings { builder ->
        //             builder.maxSize(10).minSize(2)
        //         }
        //         .applyToSocketSettings { builder ->
        //             builder.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
        //         }
        //         .build()
        // )
    }

    // Koneksi ke Atlas (MongoDB Cloud)
    private fun buatClientAtlas(): MongoClient {
        val uri = System.getenv("MONGODB_URI")
            ?: "mongodb+srv://user:[email protected]/?retryWrites=true&w=majority"
        return MongoClients.create(uri)
    }

    fun database(nama: String = "myapp"): MongoDatabase {
        return client.getDatabase(nama)
    }

    fun tutup() = client.close()
}

Model Data dan Serialisasi #

MongoDB menyimpan dokumen BSON. Untuk bekerja dengan data class Kotlin, ada dua pendekatan:

Menggunakan Document (Pendekatan Fleksibel) #

import org.bson.Document
import org.bson.types.ObjectId

// Buat dokumen secara manual
val dokumen = Document()
    .append("nama", "Laptop Gaming")
    .append("harga", 15_000_000.0)
    .append("stok", 10)
    .append("kategori", "Elektronik")
    .append("tag", listOf("gaming", "laptop", "premium"))
    .append("spesifikasi", Document()
        .append("ram", "32GB")
        .append("storage", "1TB SSD")
        .append("gpu", "RTX 4070")
    )

// Akses nilai dari dokumen
val nama = dokumen.getString("nama")
val harga = dokumen.getDouble("harga")
@Suppress("UNCHECKED_CAST")
val tag = dokumen.get("tag") as List<String>
val spek = dokumen.get("spesifikasi", Document::class.java)
println(spek?.getString("ram"))  // 32GB

Menggunakan Data Class dengan KMongo dan Serialization #

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import org.bson.codecs.pojo.annotations.BsonId
import org.bson.types.ObjectId

@Serializable
data class Spesifikasi(
    val ram: String? = null,
    val storage: String? = null,
    val gpu: String? = null,
    val cpu: String? = null
)

@Serializable
data class Produk(
    @SerialName("_id")
    val id: String = ObjectId().toHexString(),  // MongoDB menggunakan _id
    val nama: String,
    val harga: Double,
    val stok: Int = 0,
    val kategori: String? = null,
    val tag: List<String> = emptyList(),
    val spesifikasi: Spesifikasi? = null,
    val aktif: Boolean = true
)

CRUD dengan Driver Resmi #

import com.mongodb.client.MongoCollection
import com.mongodb.client.model.*
import com.mongodb.client.result.*
import org.bson.Document
import org.bson.conversions.Bson
import org.bson.types.ObjectId

class ProdukRepositoryMongo {

    private val koleksi: MongoCollection<Document> =
        KoneksiMongo.database().getCollection("produk")

    // CREATE — insertOne
    fun simpan(produk: Produk): String {
        val dokumen = Document()
            .append("nama", produk.nama)
            .append("harga", produk.harga)
            .append("stok", produk.stok)
            .append("kategori", produk.kategori)
            .append("tag", produk.tag)
            .append("aktif", produk.aktif)

        produk.spesifikasi?.let { spek ->
            dokumen.append("spesifikasi", Document()
                .append("ram", spek.ram)
                .append("storage", spek.storage)
                .append("gpu", spek.gpu)
            )
        }

        val hasil = koleksi.insertOne(dokumen)
        return hasil.insertedId?.asObjectId()?.value?.toHexString()
            ?: throw RuntimeException("Gagal menyimpan produk")
    }

    // CREATE — insertMany (batch)
    fun simpanBanyak(daftar: List<Produk>): Int {
        val dokumen = daftar.map { p ->
            Document("nama", p.nama)
                .append("harga", p.harga)
                .append("stok", p.stok)
                .append("kategori", p.kategori)
                .append("aktif", p.aktif)
        }
        val hasil = koleksi.insertMany(dokumen)
        return hasil.insertedIds.size
    }

    // READ — findOne
    fun cariById(id: String): Document? {
        return koleksi.find(Filters.eq("_id", ObjectId(id))).firstOrNull()
    }

    // READ — find dengan filter
    fun cariSemua(
        kategori: String? = null,
        hargaMaks: Double? = null,
        aktifSaja: Boolean = true,
        limit: Int = 20,
        skip: Int = 0
    ): List<Document> {
        val filter = mutableListOf<Bson>()

        if (aktifSaja) filter.add(Filters.eq("aktif", true))
        if (kategori != null) filter.add(Filters.eq("kategori", kategori))
        if (hargaMaks != null) filter.add(Filters.lte("harga", hargaMaks))

        val kueri = if (filter.isEmpty()) Document() else Filters.and(filter)

        return koleksi.find(kueri)
            .sort(Sorts.ascending("nama"))
            .skip(skip)
            .limit(limit)
            .into(mutableListOf())
    }

    // READ — cari dengan regex (case-insensitive search)
    fun cariByNama(kataKunci: String): List<Document> {
        val filter = Filters.regex("nama", kataKunci, "i")  // "i" = case-insensitive
        return koleksi.find(filter).into(mutableListOf())
    }

    // READ — dengan projection (hanya ambil field tertentu)
    fun cariNamaHarga(): List<Document> {
        val projection = Projections.fields(
            Projections.include("nama", "harga", "kategori"),
            Projections.excludeId()  // sembunyikan _id
        )
        return koleksi.find()
            .projection(projection)
            .into(mutableListOf())
    }

    // UPDATE — updateOne
    fun perbaruiHarga(id: String, hargaBaru: Double): Boolean {
        val filter = Filters.eq("_id", ObjectId(id))
        val update = Updates.set("harga", hargaBaru)
        val hasil = koleksi.updateOne(filter, update)
        return hasil.modifiedCount > 0
    }

    // UPDATE — update banyak field sekaligus
    fun perbarui(id: String, nama: String, harga: Double, stok: Int): Boolean {
        val filter = Filters.eq("_id", ObjectId(id))
        val update = Updates.combine(
            Updates.set("nama", nama),
            Updates.set("harga", harga),
            Updates.set("stok", stok),
            Updates.currentDate("diperbarui_pada")  // set timestamp otomatis
        )
        return koleksi.updateOne(filter, update).modifiedCount > 0
    }

    // UPDATE — tambah ke array (push)
    fun tambahTag(id: String, tag: String): Boolean {
        val filter = Filters.eq("_id", ObjectId(id))
        val update = Updates.addToSet("tag", tag)  // addToSet hindari duplikat
        return koleksi.updateOne(filter, update).modifiedCount > 0
    }

    // UPDATE — increment nilai numerik
    fun tambahStok(id: String, jumlah: Int): Boolean {
        val filter = Filters.eq("_id", ObjectId(id))
        val update = Updates.inc("stok", jumlah)  // atomik increment
        return koleksi.updateOne(filter, update).modifiedCount > 0
    }

    // UPSERT — insert jika tidak ada, update jika ada
    fun upsert(nama: String, harga: Double, stok: Int): String {
        val filter = Filters.eq("nama", nama)
        val update = Updates.combine(
            Updates.setOnInsert("nama", nama),
            Updates.set("harga", harga),
            Updates.set("stok", stok),
            Updates.setOnInsert("aktif", true),
            Updates.currentDate("diperbarui_pada")
        )
        val options = UpdateOptions().upsert(true)
        val hasil = koleksi.updateOne(filter, update, options)

        return hasil.upsertedId?.asObjectId()?.value?.toHexString()
            ?: filter.toBsonDocument().getString("nama").value
    }

    // DELETE — deleteOne
    fun hapus(id: String): Boolean {
        val hasil = koleksi.deleteOne(Filters.eq("_id", ObjectId(id)))
        return hasil.deletedCount > 0
    }

    // Soft delete
    fun nonaktifkan(id: String): Boolean {
        return koleksi.updateOne(
            Filters.eq("_id", ObjectId(id)),
            Updates.set("aktif", false)
        ).modifiedCount > 0
    }
}

Aggregation Pipeline #

Aggregation adalah cara MongoDB memproses dokumen melalui serangkaian tahap — setara dengan GROUP BY, JOIN, dan window function di SQL:

import com.mongodb.client.model.Accumulators
import com.mongodb.client.model.Aggregates

fun statistikPerKategori(): List<Document> {
    val pipeline = listOf(
        // Stage 1: filter hanya produk aktif
        Aggregates.match(Filters.eq("aktif", true)),

        // Stage 2: kelompokkan berdasarkan kategori
        Aggregates.group(
            "\$kategori",  // group key
            Accumulators.sum("totalProduk", 1),
            Accumulators.sum("totalStok", "\$stok"),
            Accumulators.avg("rataHarga", "\$harga"),
            Accumulators.min("hargaMin", "\$harga"),
            Accumulators.max("hargaMax", "\$harga")
        ),

        // Stage 3: urutkan berdasarkan jumlah produk
        Aggregates.sort(Sorts.descending("totalProduk")),

        // Stage 4: format output dengan $project
        Aggregates.project(
            Document("_id", 0)
                .append("kategori", "\$_id")
                .append("totalProduk", 1)
                .append("totalStok", 1)
                .append("rataHarga", Document("\$round", listOf("\$rataHarga", 0)))
                .append("hargaMin", 1)
                .append("hargaMax", 1)
        )
    )

    return KoneksiMongo.database()
        .getCollection("produk")
        .aggregate(pipeline)
        .into(mutableListOf())
}

// Contoh aggregation dengan unwind (expand array)
fun statistikPerTag(): List<Document> {
    val pipeline = listOf(
        Aggregates.match(Filters.eq("aktif", true)),
        Aggregates.unwind("\$tag"),           // pecah array tag menjadi dokumen terpisah
        Aggregates.group(
            "\$tag",
            Accumulators.sum("jumlah", 1),
            Accumulators.push("produk", "\$nama")
        ),
        Aggregates.sort(Sorts.descending("jumlah")),
        Aggregates.limit(10)                   // top 10 tag
    )

    return KoneksiMongo.database()
        .getCollection("produk")
        .aggregate(pipeline)
        .into(mutableListOf())
}

Membuat Index #

import com.mongodb.client.model.IndexOptions
import com.mongodb.client.model.Indexes

fun buatIndex() {
    val koleksi = KoneksiMongo.database().getCollection("produk")

    // Index tunggal
    koleksi.createIndex(Indexes.ascending("kategori"))
    koleksi.createIndex(Indexes.descending("harga"))

    // Index komposit
    koleksi.createIndex(
        Indexes.compoundIndex(
            Indexes.ascending("kategori"),
            Indexes.descending("harga")
        )
    )

    // Unique index
    koleksi.createIndex(
        Indexes.ascending("nama"),
        IndexOptions().unique(true)
    )

    // Index text (untuk full-text search)
    koleksi.createIndex(
        Indexes.compoundIndex(
            Indexes.text("nama"),
            Indexes.text("deskripsi")
        )
    )

    // Index pada field nested
    koleksi.createIndex(Indexes.ascending("spesifikasi.ram"))

    // TTL index — dokumen otomatis dihapus setelah N detik
    koleksi.createIndex(
        Indexes.ascending("dibuat_pada"),
        IndexOptions().expireAfter(30L, java.util.concurrent.TimeUnit.DAYS)
    )
}

// Full-text search menggunakan index text
fun cariTeks(kataKunci: String): List<Document> {
    return KoneksiMongo.database()
        .getCollection("produk")
        .find(Filters.text(kataKunci))
        .projection(Projections.metaTextScore("score"))
        .sort(Sorts.metaTextScore("score"))
        .into(mutableListOf())
}

KMongo — Pendekatan Idiomatis Kotlin #

KMongo adalah wrapper yang membuat interaksi dengan MongoDB lebih Kotlin-idiomatic, dengan dukungan penuh untuk data class dan kotlinx.serialization:

// build.gradle.kts
// implementation("org.litote.kmongo:kmongo-serialization:4.11.0")

import org.litote.kmongo.*
import org.litote.kmongo.serialization.registerSerializer

@Serializable
data class Produk(
    val id: String = newId<Produk>().toString(),
    val nama: String,
    val harga: Double,
    val stok: Int = 0,
    val kategori: String? = null,
    val aktif: Boolean = true
)

fun contohKMongo() {
    // Setup KMongo dengan serialization
    val client = KMongo.createClient("mongodb://localhost:27017")
    val db = client.getDatabase("myapp")
    val koleksi = db.getCollection<Produk>("produk")

    // INSERT
    val produk = Produk(nama = "Laptop", harga = 15_000_000.0, stok = 5)
    koleksi.insertOne(produk)

    // FIND — type-safe dengan data class
    val laptop = koleksi.findOne(Produk::nama eq "Laptop")
    println(laptop?.harga)  // 15000000.0

    // FIND dengan filter idiomatis
    val mahal = koleksi.find(Produk::harga gt 10_000_000.0).toList()
    println(mahal.size)

    // FIND dengan multiple filter
    val aktifMahal = koleksi.find(
        and(
            Produk::aktif eq true,
            Produk::harga gt 5_000_000.0
        )
    ).sort(ascending(Produk::nama)).toList()

    // UPDATE
    koleksi.updateOne(
        Produk::nama eq "Laptop",
        setValue(Produk::harga, 14_000_000.0)
    )

    // DELETE
    koleksi.deleteOne(Produk::nama eq "Laptop")

    // UPSERT
    koleksi.updateOne(
        Produk::nama eq "Keyboard",
        produk.copy(nama = "Keyboard"),
        upsert()
    )

    client.close()
}

Transaksi Multi-Dokumen #

MongoDB mendukung transaksi ACID sejak versi 4.0 (membutuhkan replica set atau sharded cluster):

import com.mongodb.client.ClientSession
import com.mongodb.TransactionOptions
import com.mongodb.ReadConcern
import com.mongodb.WriteConcern

fun transferStok(dariId: String, keId: String, jumlah: Int) {
    val client = MongoClients.create("mongodb://localhost:27017")
    val db = client.getDatabase("myapp")
    val koleksi = db.getCollection("produk")

    val session: ClientSession = client.startSession()

    val txOptions = TransactionOptions.builder()
        .readConcern(ReadConcern.SNAPSHOT)
        .writeConcern(WriteConcern.MAJORITY)
        .build()

    try {
        session.startTransaction(txOptions)

        // Kurangi stok dari produk sumber
        val hasilKurang = koleksi.updateOne(
            session,
            Filters.and(
                Filters.eq("_id", ObjectId(dariId)),
                Filters.gte("stok", jumlah)  // pastikan stok cukup
            ),
            Updates.inc("stok", -jumlah)
        )

        if (hasilKurang.modifiedCount == 0L) {
            throw IllegalStateException("Stok tidak cukup atau produk tidak ditemukan")
        }

        // Tambah stok ke produk tujuan
        koleksi.updateOne(
            session,
            Filters.eq("_id", ObjectId(keId)),
            Updates.inc("stok", jumlah)
        )

        session.commitTransaction()
        println("Transfer berhasil: $jumlah unit dari $dariId ke $keId")

    } catch (e: Exception) {
        session.abortTransaction()
        println("Transfer dibatalkan: ${e.message}")
        throw e
    } finally {
        session.close()
        client.close()
    }
}

Tips dan Pola Desain #

Embedding vs Referencing #

EMBED dokumen jika:
  ✓ Data selalu diakses bersama (pesanan + item pesanan)
  ✓ Hubungan one-to-few (pengguna + alamat, maksimal beberapa)
  ✓ Data tidak berubah secara independen

REFERENCE (simpan ID) jika:
  ✓ Data bisa diakses secara independen (produk, pengguna)
  ✓ Hubungan many-to-many
  ✓ Dokumen terlalu besar jika di-embed (>16MB limit)
  ✓ Data sering diperbarui secara independen
// Pattern embedding — pesanan dengan item langsung di dalam
data class ItemPesanan(val produkId: String, val nama: String, val harga: Double, val jumlah: Int)
data class Pesanan(
    val id: String = ObjectId().toHexString(),
    val penggunaId: String,
    val item: List<ItemPesanan>,  // embedded — tidak perlu JOIN
    val total: Double,
    val status: String = "MENUNGGU"
)

// Pattern referencing — produk sebagai referensi (ID)
data class KeranjangItem(
    val produkId: String,   // hanya simpan ID
    val jumlah: Int
)

Ringkasan #

  • MongoDB untuk data hierarki dan fleksibel — jika entitas datamu secara alami bersarang (pesanan → item → produk) dan sering diakses bersama, MongoDB menghindari JOIN yang mahal di SQL.
  • _id adalah wajib dan unik — MongoDB otomatis membuat field _id dengan ObjectId jika tidak disediakan. Selalu gunakan _id sebagai primary identifier, bukan field kustom lain.
  • Gunakan Filters, Updates, Sorts — builder classes ini membuat query lebih aman dari injeksi dan lebih mudah dibaca dibanding string query manual.
  • Updates.inc() untuk counter atomik — untuk menambah atau mengurangi nilai numerik (stok, view count), gunakan inc bukan baca-modifikasi-tulis yang rawan race condition.
  • addToSet bukan push untuk array unikaddToSet hanya menambahkan elemen jika belum ada, menghindari duplikat. push selalu menambahkan.
  • Aggregation pipeline untuk analitikmatch → group → sort → project adalah pola yang sangat umum untuk laporan agregasi. Lebih efisien dari memuat semua data ke Kotlin lalu memprosesnya.
  • Index sebelum query besar — MongoDB tanpa index melakukan collection scan yang sangat lambat. Selalu buat index pada field yang sering diquery, terutama yang ada di filter dan sort.
  • KMongo untuk kode yang lebih idiomatisProduk::nama eq "Laptop" jauh lebih aman dan lebih mudah dibaca dari Filters.eq("nama", "Laptop"). Type-safe dan auto-complete IDE bekerja dengan baik.

← Sebelumnya: PostgreSQL   Berikutnya: Elasticsearch →

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