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.
_idadalah wajib dan unik — MongoDB otomatis membuat field_iddenganObjectIdjika tidak disediakan. Selalu gunakan_idsebagai 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), gunakanincbukan baca-modifikasi-tulis yang rawan race condition.addToSetbukanpushuntuk array unik —addToSethanya menambahkan elemen jika belum ada, menghindari duplikat.pushselalu menambahkan.- Aggregation pipeline untuk analitik —
match → group → sort → projectadalah 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 idiomatis —
Produk::nama eq "Laptop"jauh lebih aman dan lebih mudah dibaca dariFilters.eq("nama", "Laptop"). Type-safe dan auto-complete IDE bekerja dengan baik.