MongoDB

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 _id bertipe ObjectId secara default.
  • insertOne/insertMany untuk insert, find dengan builder where untuk query, updateOne/updateMany dengan builder modify untuk update.
  • where.id(ObjectId.fromHexString(id)) untuk query berdasarkan _id — jangan gunakan string langsung, tipe harus sesuai.
  • modify.set, modify.inc, modify.push, modify.pull adalah 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, $lookup secara 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.

← Sebelumnya: PostgreSQL   Berikutnya: Elasticsearch →

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