MongoDB

MongoDB #

MongoDB adalah document database yang menyimpan data sebagai dokumen BSON (Binary JSON) — berbeda dari database relasional yang menyimpan data dalam baris dan kolom yang kaku. Setiap dokumen bisa punya struktur yang berbeda, mendukung nested object secara native, dan tidak memerlukan schema yang didefinisikan terlebih dahulu. Ini membuatnya ideal untuk data yang strukturnya berevolusi, konten yang sangat bervariasi per record, atau ketika kecepatan iterasi lebih penting dari konsistensi relasional yang ketat. Di Ruby, ada dua cara utama bekerja dengan MongoDB: driver resmi mongo untuk akses tingkat rendah yang fleksibel, dan Mongoid sebagai ODM (Object Document Mapper) yang memberikan pengalaman mirip ActiveRecord di Rails.

MongoDB vs Database Relasional — Kapan Pilih Mana #

MongoDB cocok untuk:
  ✓ Data dengan struktur yang bervariasi per record (katalog produk dengan atribut berbeda)
  ✓ Dokumen bersarang yang sering dibaca bersama (artikel + komentar + tag)
  ✓ High write throughput — log, event, analytics
  ✓ Schema yang sering berubah — startup dengan MVP yang cepat berevolusi
  ✓ Data geospasial dengan query $near, $geoWithin
  ✓ Horizontal scaling dengan sharding bawaan

Database relasional (PostgreSQL/MySQL) lebih baik untuk:
  ✓ Data yang sangat relasional — banyak JOIN antar tabel
  ✓ Transaksi ACID yang ketat — perbankan, e-commerce
  ✓ Laporan kompleks dengan GROUP BY, window function
  ✓ Referential integrity yang dijamin database
  ✓ Tim yang sudah familiar dengan SQL

Instalasi dan Setup #

# Install gem mongo (driver resmi MongoDB)
gem install mongo

# Atau Mongoid (ODM untuk Rails)
gem install mongoid
# Gemfile
gem 'mongo',    '~> 2.19'   # driver resmi
gem 'mongoid',  '~> 8.1'    # ODM untuk Rails (opsional)
gem 'bson',     '~> 4.15'   # types BSON (biasanya sudah jadi dependency)

Koneksi dengan Driver mongo #

require 'mongo'

# Koneksi ke MongoDB lokal
klien = Mongo::Client.new(
  ["localhost:27017"],
  database: "toko_db"
)

# Koneksi dengan autentikasi
klien = Mongo::Client.new(
  ["localhost:27017"],
  database:  "toko_db",
  user:      "appuser",
  password:  ENV["MONGO_PASSWORD"],
  auth_source: "admin"   # database tempat user dibuat
)

# Koneksi via URI (lebih ringkas)
klien = Mongo::Client.new(ENV["MONGODB_URI"])
# MONGODB_URI=mongodb://appuser:password@localhost:27017/toko_db

# Koneksi ke MongoDB Atlas (cloud)
klien = Mongo::Client.new(
  "mongodb+srv://appuser:#{ENV['MONGO_PASSWORD']}@cluster0.xxxxx.mongodb.net/toko_db",
  server_selection_timeout: 5,
  connect_timeout:          10
)

# Opsi connection pool
klien = Mongo::Client.new(
  ["localhost:27017"],
  database:         "toko_db",
  max_pool_size:    10,
  min_pool_size:    2,
  wait_queue_timeout: 5,
  socket_timeout:   5,
  server_selection_timeout: 5
)

# Cek koneksi
puts klien.database.name
puts klien.cluster.servers.first.description.server_type

# Tutup koneksi
klien.close

Akses Collection #

# Akses collection (seperti tabel di SQL)
produk_col  = klien[:produk]        # shorthand
pengguna_col = klien.use("toko_db")[:pengguna]

# Atau dengan database eksplisit
db = klien.database
produk_col = db[:produk]

CRUD — Create, Read, Update, Delete #

Create — Menyisipkan Dokumen #

produk_col = klien[:produk]

# Insert satu dokumen
hasil = produk_col.insert_one({
  nama:        "Laptop Gaming Pro",
  harga:       22_000_000,
  stok:        10,
  kategori:    "Elektronik",
  tag:         ["laptop", "gaming", "high-performance"],
  spesifikasi: {
    prosesor: "Intel Core i9",
    ram:      "32GB DDR5",
    storage:  "1TB NVMe SSD",
    gpu:      "NVIDIA RTX 4070"
  },
  aktif:       true,
  created_at:  Time.now
})

puts hasil.inserted_id   # => BSON::ObjectId('...')
puts hasil.n             # => 1

# Insert banyak dokumen sekaligus
produk_list = [
  { nama: "Mouse Wireless",  harga: 350_000, stok: 50, kategori: "Aksesori" },
  { nama: "Keyboard Mech",   harga: 750_000, stok: 30, kategori: "Aksesori" },
  { nama: "Monitor 4K 27\"", harga: 5_500_000, stok: 8, kategori: "Monitor" }
]

hasil = produk_col.insert_many(produk_list)
puts hasil.inserted_count        # => 3
puts hasil.inserted_ids.inspect  # array of ObjectId

# BSON::ObjectId — ID unik MongoDB
id_baru = BSON::ObjectId.new
puts id_baru.to_s     # => "64f1a2b3c4d5e6f7a8b9c0d1"
puts id_baru.generation_time  # => waktu ID dibuat

Read — Query Dokumen #

produk_col = klien[:produk]

# find — mengembalikan cursor (lazy, belum eksekusi)
semua = produk_col.find   # semua dokumen

# Iterasi cursor
semua.each do |dok|
  puts "#{dok['nama']}: Rp #{dok['harga']}"
end

# first — satu dokumen pertama
satu = produk_col.find({ nama: "Laptop Gaming Pro" }).first
puts satu.inspect

# Filter query
produk_col.find({ kategori: "Elektronik" }).each { |d| puts d['nama'] }

# Query operator
# $gt, $lt, $gte, $lte, $ne — perbandingan
produk_col.find({ harga: { "$gt" => 1_000_000 } }).to_a
produk_col.find({ harga: { "$gte" => 500_000, "$lte" => 5_000_000 } }).to_a

# $in, $nin — dalam / tidak dalam array
produk_col.find({ kategori: { "$in" => ["Elektronik", "Monitor"] } }).to_a
produk_col.find({ tag: "gaming" }).to_a   # cocokkan dalam array

# $regex — pencarian teks dengan regex
produk_col.find({ nama: { "$regex" => "laptop", "$options" => "i" } }).to_a

# $exists — cek keberadaan field
produk_col.find({ "spesifikasi.gpu" => { "$exists" => true } }).to_a

# $and, $or — logika
produk_col.find({
  "$and" => [
    { harga: { "$lt" => 10_000_000 } },
    { stok:  { "$gt" => 0 } },
    { aktif: true }
  ]
}).to_a

produk_col.find({
  "$or" => [
    { kategori: "Elektronik" },
    { kategori: "Monitor" }
  ]
}).to_a

# Nested document query (dot notation)
produk_col.find({ "spesifikasi.ram" => "32GB DDR5" }).to_a

# Projection — pilih field yang diambil
produk_col.find({ aktif: true }).projection({ nama: 1, harga: 1, _id: 0 }).to_a
# hanya ambil nama dan harga, abaikan _id

# Sort, limit, skip
produk_col.find.sort({ harga: -1 }).limit(10).skip(20).to_a
# -1 = descending, 1 = ascending

Update — Memperbarui Dokumen #

produk_col = klien[:produk]

# update_one — perbarui satu dokumen
produk_col.update_one(
  { nama: "Laptop Gaming Pro" },   # filter
  { "$set" => { harga: 21_000_000, updated_at: Time.now } }  # update
)

# update_many — perbarui semua yang cocok
produk_col.update_many(
  { kategori: "Aksesori" },
  { "$set" => { diskon_persen: 10, updated_at: Time.now } }
)

# Operator update
# $set — set nilai field
# $unset — hapus field
# $inc — tambah/kurangi nilai numerik
# $push — tambahkan elemen ke array
# $pull — hapus elemen dari array
# $addToSet — tambah ke array hanya jika belum ada

# Kurangi stok saat ada pembelian
produk_col.update_one(
  { _id: produk_id, stok: { "$gte" => jumlah } },
  {
    "$inc" => { stok: -jumlah },
    "$set" => { updated_at: Time.now }
  }
)

# Tambahkan tag baru ke array
produk_col.update_one(
  { _id: produk_id },
  { "$addToSet" => { tag: "promo-lebaran" } }
)

# Hapus tag tertentu
produk_col.update_one(
  { _id: produk_id },
  { "$pull" => { tag: "promo-lebaran" } }
)

# Hapus field
produk_col.update_many(
  { diskon_persen: { "$exists" => true } },
  { "$unset" => { diskon_persen: "" } }
)

# Upsert — insert jika tidak ada, update jika ada
produk_col.update_one(
  { sku: "SKU-LAPTOP-001" },
  { "$set" => { nama: "Laptop X1", harga: 15_000_000 },
    "$setOnInsert" => { created_at: Time.now } },
  upsert: true
)

# find_one_and_update — update dan kembalikan dokumen
dok_lama = produk_col.find_one_and_update(
  { _id: produk_id },
  { "$inc" => { stok: -1 } },
  return_document: :after   # kembalikan dokumen SETELAH update
)
puts dok_lama["stok"]

Delete — Menghapus Dokumen #

# delete_one — hapus satu dokumen pertama yang cocok
produk_col.delete_one({ sku: "SKU-LAMA-001" })

# delete_many — hapus semua yang cocok
produk_col.delete_many({ aktif: false, stok: 0 })

# Soft delete — lebih aman untuk data penting
produk_col.update_one(
  { _id: produk_id },
  { "$set" => { deleted_at: Time.now, aktif: false } }
)

Aggregation Pipeline #

Aggregation Pipeline adalah cara MongoDB yang powerful untuk transformasi dan analisis data — setara dengan GROUP BY, JOIN, dan window function di SQL:

# Pipeline — array of stage
pipeline = [
  # Stage 1: Filter
  { "$match" => { aktif: true } },

  # Stage 2: Lookup (seperti LEFT JOIN)
  {
    "$lookup" => {
      from:         "kategori",           # collection yang di-join
      localField:   "kategori_id",        # field di collection ini
      foreignField: "_id",                # field di collection yang di-join
      as:           "info_kategori"       # nama field hasil join (array)
    }
  },

  # Stage 3: Unwind array hasil lookup
  { "$unwind" => { path: "$info_kategori", preserveNullAndEmptyArrays: true } },

  # Stage 4: Proyeksi dan tambah field baru
  {
    "$addFields" => {
      nama_kategori: "$info_kategori.nama",
      harga_diskon:  { "$multiply" => ["$harga", 0.9] }
    }
  },

  # Stage 5: Group — hitung total dan rata-rata per kategori
  {
    "$group" => {
      _id:            "$nama_kategori",
      jumlah_produk:  { "$sum" => 1 },
      total_stok:     { "$sum" => "$stok" },
      rata_harga:     { "$avg" => "$harga" },
      harga_min:      { "$min" => "$harga" },
      harga_maks:     { "$max" => "$harga" }
    }
  },

  # Stage 6: Sort
  { "$sort" => { jumlah_produk: -1 } },

  # Stage 7: Limit
  { "$limit" => 10 }
]

hasil = produk_col.aggregate(pipeline).to_a
hasil.each do |r|
  puts "#{r['_id']}: #{r['jumlah_produk']} produk, avg Rp #{r['rata_harga'].round}"
end
# Aggregation untuk laporan penjualan per bulan
pipeline_penjualan = [
  { "$match" => { status: "selesai" } },
  {
    "$group" => {
      _id: {
        tahun: { "$year"  => "$created_at" },
        bulan: { "$month" => "$created_at" }
      },
      total_transaksi: { "$sum" => 1 },
      total_nilai:     { "$sum" => "$total" },
      rata_nilai:      { "$avg" => "$total" }
    }
  },
  {
    "$project" => {
      _id: 0,
      periode:         { "$concat" => [
        { "$toString" => "$_id.tahun" }, "-",
        { "$toString" => "$_id.bulan" }
      ]},
      total_transaksi: 1,
      total_nilai:     1,
      rata_nilai:      { "$round" => ["$rata_nilai", 0] }
    }
  },
  { "$sort" => { "periode" => 1 } }
]

pesanan_col.aggregate(pipeline_penjualan).each do |r|
  puts "#{r['periode']}: #{r['total_transaksi']} transaksi, Rp #{r['total_nilai']}"
end

Indexing #

# Buat index pada satu field
produk_col.indexes.create_one({ nama: 1 })          # ascending
produk_col.indexes.create_one({ harga: -1 })         # descending
produk_col.indexes.create_one({ sku: 1 }, unique: true)  # unique

# Compound index — untuk query yang sering menggunakan beberapa field
produk_col.indexes.create_one({ kategori: 1, harga: 1 })   # kategori dulu, lalu harga
produk_col.indexes.create_one({ aktif: 1, created_at: -1 }) # filter + sort

# Text index — full-text search
produk_col.indexes.create_one(
  { nama: "text", deskripsi: "text" },
  name: "text_search_idx",
  weights: { nama: 10, deskripsi: 5 }   # nama lebih relevan dari deskripsi
)

# Geospatial index
toko_col.indexes.create_one({ lokasi: "2dsphere" })

# Partial index — hanya index dokumen yang memenuhi kondisi
produk_col.indexes.create_one(
  { harga: 1 },
  partial_filter_expression: { aktif: { "$eq" => true } }
)

# TTL index — dokumen otomatis terhapus setelah waktu tertentu
sesi_col.indexes.create_one(
  { expired_at: 1 },
  expire_after: 0   # MongoDB menghapus ketika expired_at sudah lewat
)

# Lihat semua index
produk_col.indexes.each { |idx| puts idx.inspect }

# Full-text search menggunakan text index
produk_col.find({ "$text" => { "$search" => "laptop gaming" } })
  .projection({ score: { "$meta" => "textScore" } })
  .sort({ score: { "$meta" => "textScore" } })
  .to_a

Mongoid — ODM untuk Rails #

Mongoid adalah ODM (Object Document Mapper) yang memberikan pengalaman mirip ActiveRecord tapi untuk MongoDB:

# Inisialisasi Mongoid di Rails
rails generate mongoid:config
# config/mongoid.yml
development:
  clients:
    default:
      database: toko_development
      hosts:
        - localhost:27017
      options:
        server_selection_timeout: 5

production:
  clients:
    default:
      uri: <%= ENV["MONGODB_URI"] %>
      options:
        server_selection_timeout: 5
        max_pool_size: 10
# Model Mongoid
class Produk
  include Mongoid::Document
  include Mongoid::Timestamps    # created_at, updated_at otomatis

  # Fields
  field :nama,        type: String
  field :harga,       type: BigDecimal
  field :stok,        type: Integer,  default: 0
  field :aktif,       type: Boolean,  default: true
  field :tag,         type: Array,    default: []
  field :deskripsi,   type: String
  field :kategori_id, type: BSON::ObjectId

  # Embedded document (disimpan dalam satu dokumen yang sama)
  embeds_one  :spesifikasi
  embeds_many :gambar

  # Relasi referensi (menyimpan ID, dokumen terpisah)
  belongs_to  :kategori,  optional: true
  has_many    :ulasan,    dependent: :destroy

  # Validasi
  validates :nama,  presence: true, length: { minimum: 2, maximum: 200 }
  validates :harga, numericality: { greater_than: 0 }

  # Indexes
  index({ sku: 1 }, { unique: true, sparse: true })
  index({ nama: "text", deskripsi: "text" })
  index({ kategori_id: 1, harga: 1 })
  index({ aktif: 1, created_at: -1 })

  # Callbacks
  before_save :normalisasi_nama
  after_create :log_produk_baru

  # Scope
  scope :aktif,     -> { where(aktif: true) }
  scope :terbaru,   -> { order(created_at: :desc) }
  scope :murah,     -> { where(:harga.lt => 1_000_000) }
  scope :dengan_tag, ->(t) { where(tag: t) }

  def tersedia?
    aktif && stok > 0
  end

  private

  def normalisasi_nama
    self.nama = nama.strip if nama
  end

  def log_produk_baru
    Rails.logger.info "Produk baru: #{nama} (#{id})"
  end
end

# Embedded document
class Spesifikasi
  include Mongoid::Document
  embedded_in :produk

  field :prosesor, type: String
  field :ram,      type: String
  field :storage,  type: String
  field :gpu,      type: String
end

class Gambar
  include Mongoid::Document
  embedded_in :produk

  field :url,   type: String
  field :alt,   type: String
  field :urutan, type: Integer, default: 0
end

Query dengan Mongoid #

# Query dasar — sangat mirip ActiveRecord
Produk.all
Produk.aktif
Produk.aktif.terbaru.limit(10)
Produk.dengan_tag("gaming")
Produk.where(kategori_id: kategori.id)

# Operator query Mongoid
Produk.where(:harga.gt => 1_000_000)
Produk.where(:harga.between => (500_000..5_000_000))
Produk.where(:nama.in => ["Laptop", "Monitor"])
Produk.where(:tag.all => ["gaming", "laptop"])  # tag mengandung SEMUA

# Query nested/embedded
Produk.where("spesifikasi.ram" => "32GB DDR5")
Produk.where("gambar.0.url".exists => true)

# Full-text search
Produk.text_search("laptop gaming murah")

# Aggregation via Mongoid
Produk.collection.aggregate([
  { "$match" => { aktif: true } },
  { "$group" => { _id: "$kategori_id", total: { "$sum" => 1 } } }
]).to_a

# Create, update, delete
produk = Produk.create!(
  nama: "Laptop Pro",
  harga: 18_000_000,
  tag: ["laptop", "premium"]
)

produk.update!(harga: 17_500_000)
Produk.where(stok: 0).update_all("$set" => { aktif: false })
produk.destroy

# Embedded document
produk.build_spesifikasi(prosesor: "i9", ram: "32GB")
produk.save!

produk.gambar.create!(url: "/images/produk.jpg", urutan: 1)

Transaksi Multi-Dokumen #

MongoDB mendukung transaksi ACID sejak versi 4.0 untuk replica set:

# Transaksi sederhana
klien.start_session do |session|
  session.start_transaction(
    read_concern: { level: :snapshot },
    write_concern: { w: :majority }
  )

  begin
    # Kurangi stok
    produk_col.update_one(
      { _id: produk_id, stok: { "$gte" => 1 } },
      { "$inc" => { stok: -1 } },
      session: session
    )

    # Buat pesanan
    pesanan_col.insert_one(
      {
        produk_id: produk_id,
        pengguna_id: pengguna_id,
        jumlah: 1,
        total: harga,
        status: "pending",
        created_at: Time.now
      },
      session: session
    )

    session.commit_transaction
    puts "Transaksi berhasil"

  rescue => e
    session.abort_transaction
    puts "Transaksi dibatalkan: #{e.message}"
    raise e
  end
end

Change Streams — Real-time Monitoring #

Change Streams memungkinkan aplikasi mendengarkan perubahan data secara real-time:

# Watch perubahan pada collection produk
Thread.new do
  produk_col.watch([
    { "$match" => { "operationType" => { "$in" => ["insert", "update", "delete"] } } }
  ]) do |stream|
    stream.each do |event|
      puts "Operasi: #{event['operationType']}"
      puts "Dokumen ID: #{event['documentKey']['_id']}"
      puts "Data baru: #{event['fullDocument']&.slice('nama', 'harga')}"
      puts "---"

      # Contoh penggunaan: invalidasi cache saat data berubah
      Cache.invalidasi("produk:#{event['documentKey']['_id']}")

      # Broadcast ke WebSocket
      ActionCable.server.broadcast("produk_#{event['documentKey']['_id']}", {
        aksi: event['operationType'],
        data: event['fullDocument']
      })
    end
  end
end

GridFS — Penyimpanan File Besar #

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

require 'mongo'

klien = Mongo::Client.new(["localhost:27017"], database: "toko_db")
grid_fs = klien.database.fs

# Upload file
File.open("gambar_produk.jpg", "rb") do |file|
  file_id = grid_fs.upload_from_stream(
    "gambar_produk.jpg",
    file,
    metadata: {
      produk_id:   produk_id.to_s,
      content_type: "image/jpeg",
      ukuran:      File.size("gambar_produk.jpg")
    }
  )
  puts "File disimpan dengan ID: #{file_id}"
end

# Download file
File.open("output.jpg", "wb") do |file|
  grid_fs.download_to_stream(file_id, file)
end

# Streaming ke response (untuk web server)
# (misal di Sinatra atau Rails controller)
stream = StringIO.new
grid_fs.download_to_stream(file_id, stream)
stream.rewind
stream.read   # konten file sebagai String

# Hapus file
grid_fs.delete(file_id)

# Cari file berdasarkan nama
grid_fs.find(filename: "gambar_produk.jpg").each do |info|
  puts "#{info.filename}: #{info.length} bytes, #{info.upload_date}"
end

Pola Dokumen — Embedding vs Referencing #

Keputusan desain paling penting di MongoDB adalah apakah menyimpan data dalam satu dokumen (embedding) atau terpisah dengan referensi:

# POLA 1: Embedding — simpan dalam satu dokumen
# Gunakan ketika: data selalu dibaca bersama, tidak punya siklus hidup independen
{
  _id: BSON::ObjectId("..."),
  judul: "Tutorial Ruby",
  penulis: {
    nama: "Rina",
    email: "[email protected]"
  },
  tag: ["ruby", "pemrograman"],
  komentar: [
    { isi: "Bagus!", tanggal: Time.now },
    { isi: "Terima kasih", tanggal: Time.now }
  ]
}
# Satu query untuk artikel + penulis + tag + komentar — efisien!

# POLA 2: Referencing — simpan ID, dokumen terpisah
# Gunakan ketika: data punya siklus hidup independen, sering diakses sendiri
# atau data terlalu besar (> beberapa KB)
{
  _id: BSON::ObjectId("..."),
  judul: "Tutorial Ruby",
  penulis_id: BSON::ObjectId("penulis123"),   # referensi
  kategori_id: BSON::ObjectId("kategori456")  # referensi
}

# Pola yang sering menyebabkan masalah:
# ANTI-PATTERN: menyimpan daftar ID yang terus bertambah dalam dokumen
{
  _id: "penulis123",
  nama: "Rina",
  artikel_ids: [id1, id2, id3, ... id999999]  # bisa melebihi 16MB!
}
# BENAR: simpan referensi di sisi "banyak"
{
  _id: "artikel001",
  judul: "Tutorial Ruby",
  penulis_id: "penulis123"   # referensi di sisi artikel
}

Ringkasan #

  • MongoDB cocok untuk dokumen fleksibel, relasional untuk data terstruktur ketat — pilih berdasarkan pola akses data, bukan hype; jika banyak JOIN dan transaksi kompleks, PostgreSQL lebih tepat.
  • find mengembalikan cursor, bukan array — panggil .to_a hanya ketika butuh seluruh hasil dalam memori; iterasi cursor lebih efisien untuk dataset besar.
  • Operator $set, $inc, $push, $pull — jangan replace seluruh dokumen untuk update parsial; gunakan update operators untuk efisiensi dan keamanan concurrent access.
  • Aggregation Pipeline untuk analisis data — lebih powerful dari find biasa; $match di awal pipeline memanfaatkan index, $lookup untuk join antar collection.
  • Compound index mengikuti urutan query{ kategori: 1, harga: 1 } efisien untuk where(kategori: ...).order(harga: ...) tapi tidak untuk where(harga: ...) saja.
  • Embedding untuk data yang selalu dibaca bersama — artikel + komentar dalam satu dokumen = satu query; tapi waspadai dokumen yang tumbuh tak terbatas.
  • Referencing untuk data dengan siklus hidup independen — pengguna, kategori, dan produk masing-masing sebagai collection terpisah yang direferensikan dengan ID.
  • Text index untuk full-text search sederhana{ nama: "text", deskripsi: "text" } dengan weights untuk relevansi; untuk search yang lebih canggih, pertimbangkan Elasticsearch.
  • Transaksi MongoDB butuh Replica Set — transaksi multi-dokumen hanya tersedia jika MongoDB berjalan sebagai replica set (bukan standalone); di production, ini sudah standar.
  • Change Streams untuk real-time — dengarkan perubahan data tanpa polling; berguna untuk invalidasi cache, WebSocket broadcast, atau sinkronisasi antar sistem.

← Sebelumnya: PostgreSQL   Berikutnya: Elasticsearch →

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