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.
findmengembalikan cursor, bukan array — panggil.to_ahanya 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;
$matchdi awal pipeline memanfaatkan index,$lookupuntuk join antar collection.- Compound index mengikuti urutan query —
{ kategori: 1, harga: 1 }efisien untukwhere(kategori: ...).order(harga: ...)tapi tidak untukwhere(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.