MongoDB #

MongoDB is a document-based NoSQL database — instead of rows in tables, data is stored as flexible JSON documents (BSON internally) that can have different structures from each other. This approach fits data whose structure changes often, complex hierarchical data, or applications that need a flexible schema. The mongo_dart package provides a native Dart driver for MongoDB with full support for query operators, aggregation pipelines, indexing, and multi-document transactions.

MongoDB vs SQL — Basic Concepts #

Before diving into code, it’s important to understand the terminology differences:

SQLMongoDBDescription
DatabaseDatabaseSame — a collection of collections/tables
TableCollectionA collection of documents/rows
RowDocumentOne data record
ColumnFieldOne attribute within a document
Primary key_idUnique identifier (ObjectId or custom)
JOIN$lookup (aggregation)Combining data from other collections
IndexIndexSame — speeds up queries
TransactionSession TransactionMulti-document ACID transaction

Package Setup #

dart pub add mongo_dart
# pubspec.yaml
dependencies:
  mongo_dart: ^0.10.0

Connecting to MongoDB #

import 'package:mongo_dart/mongo_dart.dart';

Future<void> main() async {
  // Connection with a connection string
  final db = Db('mongodb://localhost:27017/toko_online');
  await db.open();

  print('Connected to MongoDB');
  print('Database: ${db.databaseName}');

  // Close the connection
  await db.close();
}

Connection Strings with Authentication #

// With a username and password
final db = Db('mongodb://username:***@localhost:27017/toko_online');

// MongoDB Atlas (cloud)
final dbAtlas = Db(
  'mongodb+srv://username:***@cluster.mongodb.net/toko_online'
  '?retryWrites=true&w=majority',
);

// With additional options
final dbDetail = Db.create(
  'mongodb://localhost:27017/toko_online',
);

await dbDetail.open();

Connection Pooling with 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('MongoDB connection opened');
    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;
  }
}

Document CRUD #

Insert #

import 'package:mongo_dart/mongo_dart.dart';

Future<void> contohInsert(DbCollection koleksi) async {
  // Insert one document
  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('Created ID: ${hasilInsert.id}');
  // MongoDB automatically adds the _id field as an ObjectId

  // Insert many documents at once
  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('Inserted: ${hasilBanyak.nInserted} documents');
}

Find — Querying Documents #

import 'package:mongo_dart/mongo_dart.dart';

Future<void> contohFind(DbCollection koleksi) async {
  // Get all documents
  final semua = await koleksi.find().toList();

  // Query with a filter
  final elektronik = await koleksi.find(
    where.eq('kategori', 'elektronik'),
  ).toList();

  // Query with multiple conditions
  final mahal = await koleksi.find(
    where.eq('aktif', true).and(where.gte('harga', 5_000_000)),
  ).toList();

  // findOne — get a single document
  final satu = await koleksi.findOne(
    where.eq('nama', 'Laptop Gaming'),
  );
  print('Found: ${satu?['nama']}');

  // Get by ObjectId
  final idString = '64f8a1b2c3d4e5f6a7b8c9d0';
  final byId = await koleksi.findOne(
    where.id(ObjectId.fromHexString(idString)),
  );

  // Field projection — choose which fields to return
  final ringkas = await koleksi.find(
    where.eq('aktif', true),
  ).map((doc) => {
    '_id': doc['_id'],
    'nama': doc['nama'],
    'harga': doc['harga'],
  }).toList();
}

Complete Query Operators #

import 'package:mongo_dart/mongo_dart.dart';

Future<void> contohOperators(DbCollection koleksi) async {
  // Comparison
  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();    // all present

  // Regex — flexible text search
  await koleksi.find(where.match('nama', 'laptop', caseInsensitive: true)).toList();

  // Null / field existence
  await koleksi.find(where.exists('diskon')).toList();          // field exists
  await koleksi.find(where.notExists('diskon')).toList();       // field doesn't exist

  // 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();

  // Nested field queries
  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 a single document
  await koleksi.updateOne(
    where.id(id),
    modify
      .set('harga', 14_500_000)
      .set('diubahPada', DateTime.now().toUtc()),
  );

  // Increment/decrement values
  await koleksi.updateOne(
    where.id(id),
    modify.inc('stok', -1),  // decrease stock by 1
  );

  // Push to an array
  await koleksi.updateOne(
    where.id(id),
    modify.push('tag', 'promo'),
  );

  // Pull from an array — remove an element
  await koleksi.updateOne(
    where.id(id),
    modify.pull('tag', 'promo'),
  );

  // addToSet — push only if not already present
  await koleksi.updateOne(
    where.id(id),
    modify.addToSet('tag', 'sale'),
  );

  // updateMany — update everything matching
  final hasil = await koleksi.updateMany(
    where.lt('stok', 5),
    modify.set('perluRestock', true),
  );
  print('Updated: ${hasil.nModified} documents');

  // findAndModify — update and return the latest document
  final updated = await koleksi.findAndModify(
    query: where.id(id),
    update: modify.set('harga', 13_000_000),
    returnNew: true,  // return the post-update version
  );
  print('New price: ${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('Deleted: ${hasil.nRemoved} documents');

  // Soft delete — safer for production
  await koleksi.updateOne(
    where.id(id),
    modify.set('aktif', false).set('dihapusPada', DateTime.now().toUtc()),
  );
}

Mapping to Model Classes #

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 with a 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 #

The aggregation pipeline is the most powerful way to process and analyze data in MongoDB — replacing GROUP BY, JOIN, and complex calculations from SQL:

import 'package:mongo_dart/mongo_dart.dart';

Future<void> contohAggregasi(DbCollection koleksi) async {
  // Total revenue per category
  final hasilAgg = await koleksi.aggregate([
    // Stage 1: filter only active products
    {'\$match': {'aktif': true}},

    // Stage 2: group by category and calculate statistics
    {
      '\$group': {
        '_id': '\$kategori',
        'totalProduk': {'\$sum': 1},
        'totalStok': {'\$sum': '\$stok'},
        'hargaRata': {'\$avg': '\$harga'},
        'hargaTertinggi': {'\$max': '\$harga'},
        'hargaTerendah': {'\$min': '\$harga'},
      }
    },

    // Stage 3: sort from most products
    {'\$sort': {'totalProduk': -1}},

    // Stage 4: rename the _id field to 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']} products, '
        'average Rp${row['hargaRata']}');
  }

  // $lookup — JOIN with another collection
  final ordersKoleksi = koleksi.db.collection('orders');
  final orderDenganUser = await ordersKoleksi.aggregate([
    {
      '\$lookup': {
        'from': 'pengguna',           // the collection being joined
        'localField': 'idPengguna',   // field in orders
        'foreignField': '_id',         // field in pengguna
        'as': 'infoPengguna',         // result field name
      }
    },
    {'\$unwind': '\$infoPengguna'},   // array → object (because lookup returns an array)
    {
      '\$project': {
        'status': 1,
        'total': 1,
        'namaPengguna': '\$infoPengguna.nama',
        'emailPengguna': '\$infoPengguna.email',
      }
    },
  ]).toList();
}

Indexing #

Indexes are crucial for query performance in MongoDB — without an index, MongoDB performs a full collection scan:

import 'package:mongo_dart/mongo_dart.dart';

Future<void> buatIndex(DbCollection koleksi) async {
  // Index on a single field
  await koleksi.createIndex(
    keys: {'kategori': 1},  // 1 = ascending, -1 = descending
  );

  // Compound index (multiple fields)
  await koleksi.createIndex(
    keys: {'kategori': 1, 'harga': -1},
  );

  // Unique index
  await koleksi.createIndex(
    keys: {'sku': 1},
    unique: true,
  );

  // Text index for full-text search
  await koleksi.createIndex(
    keys: {'nama': 'text', 'deskripsi': 'text'},
    name: 'text_search_index',
  );

  // Use text search
  final hasilTeks = await koleksi.find(
    where.raw({'\$text': {'\$search': 'laptop gaming'}}),
  ).toList();

  // TTL Index — documents automatically deleted after a duration
  // Good for sessions, caches, temporary logs
  final sesiKoleksi = koleksi.db.collection('sesi');
  await sesiKoleksi.createIndex(
    keys: {'dibuatPada': 1},
    expireAfterSeconds: 3600,  // delete after 1 hour
  );

  // See all indexes
  final semuaIndex = await koleksi.getIndexes();
  print('Indexes: $semuaIndex');
}

Multi-Document Transactions #

MongoDB supports ACID transactions across multiple documents/collections (requires a replica set or sharded cluster):

import 'package:mongo_dart/mongo_dart.dart';

Future<void> transferStok(
  Db db,
  String idDari,
  String idKe,
  int jumlah,
) async {
  // Create a session for the transaction
  final session = await db.startSession();

  try {
    await session.withTransaction(() async {
      final produkKoleksi = db.collection('produk');

      // Decrease the source stock
      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('Insufficient source stock');
      }

      // Increase the destination stock
      await produkKoleksi.updateOne(
        where.id(ObjectId.fromHexString(idKe)),
        modify.inc('stok', jumlah),
        session: session,
      );

      // Record the log
      final logKoleksi = db.collection('log_transfer');
      await logKoleksi.insertOne({
        'dari': idDari,
        'ke': idKe,
        'jumlah': jumlah,
        'tanggal': DateTime.now().toUtc(),
      }, session: session);

    });

    print('Stock transfer successful');
  } catch (e) {
    print('Transfer failed: $e');
    rethrow;
  } finally {
    await session.close();
  }
}

MongoDB Anti-Patterns #

Documents That Are Too Large #

// ANTI-PATTERN: embedding all data without consideration
// MongoDB limits documents to a maximum of 16MB
final produkBesar = {
  'nama': 'Laptop',
  // ✗ storing thousands of reviews directly in the product document
  'review': List.generate(10000, (i) => {
    'pengguna': 'User $i',
    'komentar': 'Lorem ipsum...',
    'rating': 5,
  }),
  // The document could exceed 16MB!
};

// CORRECT: create a separate collection for reviews
// 'produk' collection: only product info + rating summary
// 'review' collection: each review as a separate document referencing the product
final produkBenar = {
  'nama': 'Laptop',
  'totalReview': 0,
  'ratingRata': 0.0,
};

final reviewBaru = {
  'idProduk': ObjectId(),   // reference to the product
  'pengguna': 'User 1',
  'komentar': 'Great!',
  'rating': 5,
  'tanggal': DateTime.now(),
};

Not Creating Indexes for Frequently Queried Fields #

// ANTI-PATTERN: querying without an index on a large collection
await koleksi.find(
  where.eq('email', '[email protected]'),  // ✗ full scan if there's no index!
).toList();

// CORRECT: create the index before deployment
await koleksi.createIndex(
  keys: {'email': 1},
  unique: true,          // emails must be unique
);

// With the index, this query is very fast
await koleksi.find(
  where.eq('email', '[email protected]'),  // ✓ O(log n) with an index
).toList();

Using ObjectId as a String Without Conversion #

// ANTI-PATTERN: storing the ID as a plain string
final doc = await koleksi.findOne(
  where.eq('_id', '64f8a1b2c3d4e5f6a7b8c9d0'),  // ✗ won't match! _id is an ObjectId
);
// Always returns null because the types don't match

// CORRECT: convert to an ObjectId
final id = '64f8a1b2c3d4e5f6a7b8c9d0';
final doc2 = await koleksi.findOne(
  where.id(ObjectId.fromHexString(id)),  // ✓ correct type
);

Summary #

  • Collections and Documents — MongoDB stores data as JSON (BSON) documents in collections. Each document has an _id of type ObjectId by default.
  • insertOne/insertMany for inserts, find with the where builder for queries, updateOne/updateMany with the modify builder for updates.
  • where.id(ObjectId.fromHexString(id)) for querying by _id — don’t use a plain string, the type must match.
  • modify.set, modify.inc, modify.push, modify.pull are update operators — use these instead of sending the full document to avoid race conditions.
  • The aggregation pipeline replaces GROUP BY, JOIN, and complex SQL calculations — chain $match, $group, $sort, $project, $lookup stages in sequence.
  • Indexes are mandatory for frequently queried fields — without an index, MongoDB performs a very slow full collection scan on large data.
  • TTL Indexes (expireAfterSeconds) for automatic document deletion — ideal for sessions, tokens, and caches with an expiration period.
  • Transactions require a replica set or sharded cluster — use session.withTransaction() for atomic multi-document operations.
  • Avoid overly large documents (16MB limit) — embed data that’s always accessed together, reference other collections for unbounded growth like comments and logs.
  • Text indexes for full-text search{nama: 'text', deskripsi: 'text'} enables $text: {$search: 'keyword'} queries that are better than regex.

← Previous: PostgreSQL   Next: Elasticsearch →

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