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:
| SQL | MongoDB | Description |
|---|---|---|
| Database | Database | Same — a collection of collections/tables |
| Table | Collection | A collection of documents/rows |
| Row | Document | One data record |
| Column | Field | One attribute within a document |
| Primary key | _id | Unique identifier (ObjectId or custom) |
| JOIN | $lookup (aggregation) | Combining data from other collections |
| Index | Index | Same — speeds up queries |
| Transaction | Session Transaction | Multi-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
_idof typeObjectIdby default.insertOne/insertManyfor inserts,findwith thewherebuilder for queries,updateOne/updateManywith themodifybuilder 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.pullare 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,$lookupstages 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.