MongoDB #
MongoDB adalah database NoSQL berbasis dokumen — alih-alih baris di tabel, data disimpan sebagai dokumen JSON (secara internal BSON) yang fleksibel dan bisa punya struktur berbeda satu sama lain. Pendekatan ini sangat cocok untuk data yang strukturnya sering berubah, data hierarkis yang kompleks, atau aplikasi yang membutuhkan schema yang fleksibel. Package mongo_dart menyediakan driver native Dart untuk MongoDB dengan dukungan penuh untuk query operators, aggregation pipeline, indexing, dan transaksi multi-document.
Konsep Dasar MongoDB vs SQL #
Sebelum masuk ke kode, penting memahami perbedaan terminologi:
| SQL | MongoDB | Deskripsi |
|---|---|---|
| Database | Database | Sama — kumpulan koleksi/tabel |
| Table | Collection | Kumpulan dokumen/baris |
| Row | Document | Satu record data |
| Column | Field | Satu atribut dalam dokumen |
| Primary key | _id |
Identifier unik (ObjectId atau kustom) |
| JOIN | $lookup (aggregation) |
Menggabungkan data dari koleksi lain |
| Index | Index | Sama — mempercepat query |
| Transaction | Session Transaction | Multi-document ACID transaction |
Setup Package #
dart pub add mongo_dart
# pubspec.yaml
dependencies:
mongo_dart: ^0.10.0
Koneksi ke MongoDB #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> main() async {
// Koneksi dengan connection string
final db = Db('mongodb://localhost:27017/toko_online');
await db.open();
print('Terhubung ke MongoDB');
print('Database: ${db.databaseName}');
// Tutup koneksi
await db.close();
}
Connection String dengan Autentikasi #
// Dengan username dan password
final db = Db('mongodb://username:password@localhost:27017/toko_online');
// MongoDB Atlas (cloud)
final dbAtlas = Db(
'mongodb+srv://username:[email protected]/toko_online'
'?retryWrites=true&w=majority',
);
// Dengan opsi tambahan
final dbDetail = Db.create(
'mongodb://localhost:27017/toko_online',
);
await dbDetail.open();
Koneksi Pool dengan Db
#
import 'package:mongo_dart/mongo_dart.dart';
// Singleton database connection
class MongoDatabase {
static Db? _instance;
static Future<Db> dapatkan() async {
if (_instance != null && _instance!.isConnected) return _instance!;
_instance = await Db.create(
'mongodb://localhost:27017/toko_online',
);
await _instance!.open();
print('Koneksi MongoDB dibuka');
return _instance!;
}
static Future<DbCollection> koleksi(String nama) async {
final db = await dapatkan();
return db.collection(nama);
}
static Future<void> tutup() async {
await _instance?.close();
_instance = null;
}
}
CRUD Dokumen #
Insert #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohInsert(DbCollection koleksi) async {
// Insert satu dokumen
final produkBaru = {
'nama': 'Laptop Gaming',
'harga': 15_000_000,
'stok': 10,
'kategori': 'elektronik',
'tag': ['gaming', 'laptop', 'elektronik'],
'spesifikasi': {
'ram': '16GB',
'storage': '512GB SSD',
'cpu': 'Intel Core i7',
},
'aktif': true,
'dibuatPada': DateTime.now().toUtc(),
};
final hasilInsert = await koleksi.insertOne(produkBaru);
print('ID yang dibuat: ${hasilInsert.id}');
// MongoDB otomatis menambahkan field _id berupa ObjectId
// Insert banyak dokumen sekaligus
final banyakProduk = [
{'nama': 'Mouse Gaming', 'harga': 500_000, 'stok': 25, 'aktif': true},
{'nama': 'Keyboard Mechanical', 'harga': 750_000, 'stok': 15, 'aktif': true},
];
final hasilBanyak = await koleksi.insertMany(banyakProduk);
print('Berhasil insert: ${hasilBanyak.nInserted} dokumen');
}
Find — Query Dokumen #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohFind(DbCollection koleksi) async {
// Ambil semua dokumen
final semua = await koleksi.find().toList();
// Query dengan filter
final elektronik = await koleksi.find(
where.eq('kategori', 'elektronik'),
).toList();
// Query dengan multiple kondisi
final mahal = await koleksi.find(
where.eq('aktif', true).and(where.gte('harga', 5_000_000)),
).toList();
// findOne — ambil satu dokumen
final satu = await koleksi.findOne(
where.eq('nama', 'Laptop Gaming'),
);
print('Ditemukan: ${satu?['nama']}');
// Ambil berdasarkan ObjectId
final idString = '64f8a1b2c3d4e5f6a7b8c9d0';
final byId = await koleksi.findOne(
where.id(ObjectId.fromHexString(idString)),
);
// Field projection — pilih field yang ingin ditampilkan
final ringkas = await koleksi.find(
where.eq('aktif', true),
).map((doc) => {
'_id': doc['_id'],
'nama': doc['nama'],
'harga': doc['harga'],
}).toList();
}
Query Operators Lengkap #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohOperators(DbCollection koleksi) async {
// Perbandingan
await koleksi.find(where.gt('harga', 1_000_000)).toList(); // >
await koleksi.find(where.gte('harga', 1_000_000)).toList(); // >=
await koleksi.find(where.lt('stok', 5)).toList(); // <
await koleksi.find(where.lte('stok', 10)).toList(); // <=
await koleksi.find(where.ne('kategori', 'elektronik')).toList(); // !=
// Array operators
await koleksi.find(where.within('tag', ['gaming', 'laptop'])).toList(); // IN
await koleksi.find(where.nin('tag', ['bekas', 'rusak'])).toList(); // NOT IN
await koleksi.find(where.all('tag', ['gaming', 'laptop'])).toList(); // semua ada
// Regex — pencarian teks fleksibel
await koleksi.find(where.match('nama', 'laptop', caseInsensitive: true)).toList();
// Null / keberadaan field
await koleksi.find(where.exists('diskon')).toList(); // field ada
await koleksi.find(where.notExists('diskon')).toList(); // field tidak ada
// Sort, limit, skip
final halaman1 = await koleksi.find(
where.eq('aktif', true)
.sortBy('harga', descending: true)
.limit(10)
.skip(0),
).toList();
final halaman2 = await koleksi.find(
where.eq('aktif', true)
.sortBy('harga', descending: true)
.limit(10)
.skip(10),
).toList();
// Query nested field
await koleksi.find(
where.eq('spesifikasi.ram', '16GB'),
).toList();
}
Update #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohUpdate(DbCollection koleksi) async {
final id = ObjectId.fromHexString('64f8a1b2c3d4e5f6a7b8c9d0');
// updateOne — update satu dokumen
await koleksi.updateOne(
where.id(id),
modify
.set('harga', 14_500_000)
.set('diubahPada', DateTime.now().toUtc()),
);
// Increment/decrement nilai
await koleksi.updateOne(
where.id(id),
modify.inc('stok', -1), // kurangi stok 1
);
// Push ke array
await koleksi.updateOne(
where.id(id),
modify.push('tag', 'promo'),
);
// Pull dari array — hapus elemen
await koleksi.updateOne(
where.id(id),
modify.pull('tag', 'promo'),
);
// addToSet — push hanya jika belum ada
await koleksi.updateOne(
where.id(id),
modify.addToSet('tag', 'sale'),
);
// updateMany — update semua yang cocok
final hasil = await koleksi.updateMany(
where.lt('stok', 5),
modify.set('perluRestock', true),
);
print('Diupdate: ${hasil.nModified} dokumen');
// findAndModify — update dan kembalikan dokumen terbaru
final updated = await koleksi.findAndModify(
query: where.id(id),
update: modify.set('harga', 13_000_000),
returnNew: true, // kembalikan versi setelah update
);
print('Harga baru: ${updated?['harga']}');
}
Delete #
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohDelete(DbCollection koleksi) async {
final id = ObjectId.fromHexString('64f8a1b2c3d4e5f6a7b8c9d0');
// deleteOne
await koleksi.deleteOne(where.id(id));
// deleteMany
final hasil = await koleksi.deleteMany(
where.eq('aktif', false),
);
print('Dihapus: ${hasil.nRemoved} dokumen');
// Soft delete — lebih aman untuk produksi
await koleksi.updateOne(
where.id(id),
modify.set('aktif', false).set('dihapusPada', DateTime.now().toUtc()),
);
}
Mapping ke Model Kelas #
import 'package:mongo_dart/mongo_dart.dart';
class Produk {
final ObjectId id;
final String nama;
final double harga;
final int stok;
final String kategori;
final List<String> tag;
final Map<String, dynamic>? spesifikasi;
final bool aktif;
final DateTime dibuatPada;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.stok,
required this.kategori,
required this.tag,
this.spesifikasi,
required this.aktif,
required this.dibuatPada,
});
factory Produk.dariMap(Map<String, dynamic> map) {
return Produk(
id: map['_id'] as ObjectId,
nama: map['nama'] as String,
harga: (map['harga'] as num).toDouble(),
stok: map['stok'] as int,
kategori: map['kategori'] as String,
tag: (map['tag'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
spesifikasi: map['spesifikasi'] as Map<String, dynamic>?,
aktif: map['aktif'] as bool? ?? true,
dibuatPada: map['dibuatPada'] as DateTime? ?? DateTime.now(),
);
}
Map<String, dynamic> keMap() => {
'_id': id,
'nama': nama,
'harga': harga,
'stok': stok,
'kategori': kategori,
'tag': tag,
if (spesifikasi != null) 'spesifikasi': spesifikasi,
'aktif': aktif,
'dibuatPada': dibuatPada,
};
}
// Repository dengan typed model
class ProdukRepository {
final DbCollection _koleksi;
ProdukRepository(this._koleksi);
Future<List<Produk>> ambilSemua({int halaman = 1, int perHalaman = 20}) async {
final docs = await _koleksi.find(
where.eq('aktif', true)
.sortBy('dibuatPada', descending: true)
.limit(perHalaman)
.skip((halaman - 1) * perHalaman),
).toList();
return docs.map(Produk.dariMap).toList();
}
Future<Produk?> ambilById(String id) async {
final doc = await _koleksi.findOne(
where.id(ObjectId.fromHexString(id)),
);
return doc == null ? null : Produk.dariMap(doc);
}
Future<Produk> tambah(Produk produk) async {
final map = produk.keMap();
await _koleksi.insertOne(map);
return produk;
}
}
Aggregation Pipeline #
Aggregation pipeline adalah cara paling powerful untuk memproses dan menganalisis data di MongoDB — menggantikan GROUP BY, JOIN, dan kalkulasi kompleks dari SQL:
import 'package:mongo_dart/mongo_dart.dart';
Future<void> contohAggregasi(DbCollection koleksi) async {
// Total pendapatan per kategori
final hasilAgg = await koleksi.aggregate([
// Stage 1: filter hanya produk aktif
{'\$match': {'aktif': true}},
// Stage 2: kelompokkan per kategori dan hitung statistik
{
'\$group': {
'_id': '\$kategori',
'totalProduk': {'\$sum': 1},
'totalStok': {'\$sum': '\$stok'},
'hargaRata': {'\$avg': '\$harga'},
'hargaTertinggi': {'\$max': '\$harga'},
'hargaTerendah': {'\$min': '\$harga'},
}
},
// Stage 3: urutkan dari yang terbanyak produknya
{'\$sort': {'totalProduk': -1}},
// Stage 4: rename field _id menjadi kategori
{
'\$project': {
'kategori': '\$_id',
'_id': 0,
'totalProduk': 1,
'totalStok': 1,
'hargaRata': {'\$round': ['\$hargaRata', 0]},
'hargaTertinggi': 1,
'hargaTerendah': 1,
}
},
]).toList();
for (final row in hasilAgg) {
print('${row['kategori']}: ${row['totalProduk']} produk, '
'rata-rata Rp${row['hargaRata']}');
}
// $lookup — JOIN dengan koleksi lain
final ordersKoleksi = koleksi.db.collection('orders');
final orderDenganUser = await ordersKoleksi.aggregate([
{
'\$lookup': {
'from': 'pengguna', // koleksi yang di-join
'localField': 'idPengguna', // field di orders
'foreignField': '_id', // field di pengguna
'as': 'infoPengguna', // nama field hasil
}
},
{'\$unwind': '\$infoPengguna'}, // array → objek (karena lookup return array)
{
'\$project': {
'status': 1,
'total': 1,
'namaPengguna': '\$infoPengguna.nama',
'emailPengguna': '\$infoPengguna.email',
}
},
]).toList();
}
Indexing #
Index sangat penting untuk performa query di MongoDB — tanpa index, MongoDB melakukan full collection scan:
import 'package:mongo_dart/mongo_dart.dart';
Future<void> buatIndex(DbCollection koleksi) async {
// Index pada satu field
await koleksi.createIndex(
keys: {'kategori': 1}, // 1 = ascending, -1 = descending
);
// Index compound (beberapa field)
await koleksi.createIndex(
keys: {'kategori': 1, 'harga': -1},
);
// Index unique
await koleksi.createIndex(
keys: {'sku': 1},
unique: true,
);
// Index text untuk full-text search
await koleksi.createIndex(
keys: {'nama': 'text', 'deskripsi': 'text'},
name: 'text_search_index',
);
// Gunakan text search
final hasilTeks = await koleksi.find(
where.raw({'\$text': {'\$search': 'laptop gaming'}}),
).toList();
// TTL Index — dokumen otomatis dihapus setelah durasi tertentu
// Cocok untuk session, cache, log sementara
final sesiKoleksi = koleksi.db.collection('sesi');
await sesiKoleksi.createIndex(
keys: {'dibuatPada': 1},
expireAfterSeconds: 3600, // hapus setelah 1 jam
);
// Lihat semua index
final semuaIndex = await koleksi.getIndexes();
print('Index: $semuaIndex');
}
Transaksi Multi-Document #
MongoDB mendukung transaksi ACID untuk beberapa dokumen/koleksi sekaligus (membutuhkan replica set atau sharded cluster):
import 'package:mongo_dart/mongo_dart.dart';
Future<void> transferStok(
Db db,
String idDari,
String idKe,
int jumlah,
) async {
// Buat session untuk transaksi
final session = await db.startSession();
try {
await session.withTransaction(() async {
final produkKoleksi = db.collection('produk');
// Kurangi stok sumber
final hasilKurang = await produkKoleksi.updateOne(
where.id(ObjectId.fromHexString(idDari)).gte('stok', jumlah),
modify.inc('stok', -jumlah),
session: session,
);
if (hasilKurang.nModified == 0) {
throw Exception('Stok sumber tidak mencukupi');
}
// Tambah stok tujuan
await produkKoleksi.updateOne(
where.id(ObjectId.fromHexString(idKe)),
modify.inc('stok', jumlah),
session: session,
);
// Catat log
final logKoleksi = db.collection('log_transfer');
await logKoleksi.insertOne({
'dari': idDari,
'ke': idKe,
'jumlah': jumlah,
'tanggal': DateTime.now().toUtc(),
}, session: session);
});
print('Transfer stok berhasil');
} catch (e) {
print('Transfer gagal: $e');
rethrow;
} finally {
await session.close();
}
}
Anti-Pattern MongoDB #
Dokumen yang Terlalu Besar #
// ANTI-PATTERN: embed semua data tanpa pertimbangan
// MongoDB membatasi dokumen maksimal 16MB
final produkBesar = {
'nama': 'Laptop',
// ✗ menyimpan ribuan review langsung di dokumen produk
'review': List.generate(10000, (i) => {
'pengguna': 'User $i',
'komentar': 'Lorem ipsum...',
'rating': 5,
}),
// Dokumen bisa melebihi 16MB!
};
// BENAR: buat koleksi terpisah untuk review
// Koleksi 'produk': hanya info produk + summary rating
// Koleksi 'review': setiap review sebagai dokumen terpisah dengan reference ke produk
final produkBenar = {
'nama': 'Laptop',
'totalReview': 0,
'ratingRata': 0.0,
};
final reviewBaru = {
'idProduk': ObjectId(), // reference ke produk
'pengguna': 'User 1',
'komentar': 'Bagus!',
'rating': 5,
'tanggal': DateTime.now(),
};
Tidak Membuat Index untuk Field yang Sering Diquery #
// ANTI-PATTERN: query tanpa index pada collection besar
await koleksi.find(
where.eq('email', '[email protected]'), // ✗ full scan jika tanpa index!
).toList();
// BENAR: buat index sebelum deployment
await koleksi.createIndex(
keys: {'email': 1},
unique: true, // email harus unik
);
// Setelah ada index, query ini sangat cepat
await koleksi.find(
where.eq('email', '[email protected]'), // ✓ O(log n) dengan index
).toList();
Menggunakan ObjectId sebagai String Tanpa Konversi #
// ANTI-PATTERN: simpan ID sebagai string biasa
final doc = await koleksi.findOne(
where.eq('_id', '64f8a1b2c3d4e5f6a7b8c9d0'), // ✗ tidak cocok! _id bertipe ObjectId
);
// Selalu mengembalikan null karena tipe tidak cocok
// BENAR: konversi ke ObjectId
final id = '64f8a1b2c3d4e5f6a7b8c9d0';
final doc2 = await koleksi.findOne(
where.id(ObjectId.fromHexString(id)), // ✓ tipe benar
);
Ringkasan #
- Collection dan Document — MongoDB menyimpan data sebagai dokumen JSON (BSON) dalam collection. Setiap dokumen punya
_idbertipeObjectIdsecara default.insertOne/insertManyuntuk insert,finddengan builderwhereuntuk query,updateOne/updateManydengan buildermodifyuntuk update.where.id(ObjectId.fromHexString(id))untuk query berdasarkan_id— jangan gunakan string langsung, tipe harus sesuai.modify.set,modify.inc,modify.push,modify.pulladalah update operators — gunakan ini daripada mengirim dokumen penuh untuk menghindari race condition.- Aggregation pipeline menggantikan GROUP BY, JOIN, dan kalkulasi kompleks SQL — susun stage
$match,$group,$sort,$project,$lookupsecara berurutan.- Index wajib untuk field yang sering diquery — tanpa index, MongoDB melakukan full collection scan yang sangat lambat untuk data besar.
- TTL Index (
expireAfterSeconds) untuk menghapus dokumen secara otomatis — ideal untuk session, token, dan cache dengan masa kadaluarsa.- Transaksi membutuhkan replica set atau sharded cluster — gunakan
session.withTransaction()untuk operasi multi-document yang atomic.- Hindari dokumen terlalu besar (limit 16MB) — embed data yang selalu diakses bersama, referensi ke koleksi lain untuk data yang tumbuh tidak terbatas seperti komentar dan log.
- Text index untuk full-text search —
{nama: 'text', deskripsi: 'text'}memungkinkan query$text: {$search: 'keyword'}yang lebih baik dari regex.