Old vs New: Legacy Methods & MMAPv1
MongoDB has been around since 2009, and the internet is full of tutorials, Stack Overflow answers and company code written for versions that are long out of support. You will meet db.users.insert(...), find().count(), mapReduce and talk of the MMAPv1 storage engine. Some of it still runs, some prints warnings and some no longer works at all.
This lesson is your translation guide: for each old feature, what it did, why it changed and what to write instead. Start with freshly loaded data and use shop.
The shell: mongo became mongosh
The original shell was called mongo. It was replaced by mongosh, and it was removed from the server downloads in MongoDB 6.0. Most commands look the same, but the details differ:
Legacy mongo shell | mongosh |
|---|---|
Output with double quotes: "name" : "Pen" | Output with single quotes and unquoted keys: name: 'Pen' |
| All numbers stored as doubles | Whole numbers that fit in 32 bits stored as int |
find().pretty() needed for readable output | Output is formatted already; pretty() does nothing |
ObjectId("...").str gives the hex string | Use .toString() |
Startup file .mongorc.js | Startup file .mongoshrc.js |
DBQuery.shellBatchSize = 50 | config.set('displayBatchSize', 50) |
The number change matters for data: a collection filled half by the old shell and half by mongosh can hold qty: 1 as a double in some documents and an int in others, which confuses code that checks types. See BSON Data Types & ObjectId.
Deprecated CRUD methods
The old CRUD methods were replaced by methods whose names say exactly what they do. mongosh still runs most of the old ones, but prints a DeprecationWarning the first time you use each in a session:
| Old method | Use instead | Why it changed |
|---|---|---|
insert(docOrArray) | insertOne() / insertMany() | One method with two behaviours was confusing |
update(filter, doc, { multi }) | updateOne() / updateMany() / replaceOne() | Easy to replace a whole document by accident |
remove(filter, justOne) | deleteOne() / deleteMany() | Deleted all matches unless told otherwise |
count() and find().count() | countDocuments() / estimatedDocumentCount() | Could return wrong numbers |
save(doc) | insertOne() or replaceOne() with upsert | Not available in mongosh at all |
copyTo() | An aggregation ending in $out | Removed server feature |
mapReduce() | The aggregation pipeline | Slower and harder to write |
ensureIndex() | createIndex() | Old name kept only for compatibility |
Here is the warning you see for count(), next to its modern replacement. Both return 5 accessories on fresh data:
db.products.count({ category: 'Accessories' })
db.products.countDocuments({ category: 'Accessories' })DeprecationWarning: Collection.count() is deprecated. Use countDocuments or estimatedDocumentCount. 5 5
The answers match here, but count() without a filter reads a number from the collection's metadata, which can be wrong after an unclean shutdown or on a sharded cluster. countDocuments() always counts the matching documents, and estimatedDocumentCount() is the honest name for the fast metadata version.
insert() still works, warns, and returns the same result shape as insertMany():
db.notes.insert({ text: 'Restock USB-C cables before Diwali' })DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
acknowledged: true,
insertedIds: { '0': ObjectId('6ae45a1cc3f5e8a91d2b7a0f') }
}The update() trap
In the legacy shell, update() with a plain document replaced the whole matching document:
// Legacy shell: this replaced product 104 with { _id: 104, price: 249 }
db.products.update({ _id: 104 }, { price: 249 })Every other field (name, category, stock, tags) silently disappeared. Countless production bugs came from this. In mongosh, update() passes your document to the modern update path, which refuses a document without update operators, so the line above now fails with MongoInvalidArgumentError: Update document requires atomic operators. The fix is to say what you mean:
db.products.updateOne({ _id: 104 }, { $set: { price: 249 } }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}Use replaceOne() on the rare occasions when you really want to replace a document. Reload the shop data afterwards.
mapReduce versus aggregation
mapReduce ran JavaScript functions on the server to group and summarise data. It was deprecated in MongoDB 5.0 in favour of the aggregation pipeline, which is faster, uses indexes and is easier to read. A legacy "revenue per status" job and its modern replacement:
// Legacy: deprecated
db.orders.mapReduce(
function () { emit(this.status, this.total); },
function (key, values) { return Array.sum(values); },
{ out: { inline: 1 } }
)
// Modern
db.orders.aggregate([{ $group: { _id: '$status', revenue: { $sum: '$total' } } }])Other server-side JavaScript has a more mixed history: $where, $function and $accumulator were deprecated in 8.0 and un-deprecated in 9.0, which runs them on a new WebAssembly-based engine. They are still slower than native operators, so prefer $expr and the aggregation operators you learned in The Aggregation Pipeline. Older commands such as group and db.eval() were removed long ago.
Storage engines: MMAPv1 and WiredTiger
The storage engine is the part of the server that writes documents to disk. MongoDB's original engine, MMAPv1, mapped data files directly into memory. It was simple but had serious limits:
- Coarse locking. Writes locked a whole database in early versions and a whole collection later, so busy collections queued up.
- No compression. Data used much more disk space.
- Wasted space. Documents that grew had to be moved, leaving gaps, and files had to be padded.
WiredTiger became the default engine in MongoDB 3.2. It brought document-level concurrency (two writes to different documents in one collection do not block each other), compression of data and indexes (Snappy by default, with zlib and zstd available), checkpoints plus a write-ahead journal for crash recovery, and the foundation for transactions. MMAPv1 was deprecated in 4.0 and removed in 4.2. If an old article tunes MMAPv1 settings such as smallfiles or padding factors, ignore it; every supported server runs WiredTiger.
Other legacy patterns in application code
- Callbacks in drivers. Node.js driver code with
function (err, result)callbacks predates driver 5.0, which removed them. Useasync/await, as in The Node.js Driver. - Old connection options.
useNewUrlParseranduseUnifiedTopologyhave had no effect since driver 4 and should be deleted. background: trueon index builds. Ignored since 4.2, where all builds use the same optimised process.- Very old drivers. Servers since 6.0 no longer accept some outdated network message formats, so applications using very old drivers cannot connect at all. Upgrade drivers before you upgrade the server.
Common mistakes
- Copying old tutorials line by line. Check every method against the modern list above before using it.
- Ignoring DeprecationWarnings. They mark code that may break in a later release; fix them when you see them.
- Assuming
count()is exact. UsecountDocuments()when the number matters. - Mixing shells on one collection. Old-shell doubles and
mongoshints in the same field cause type surprises.
Key points
- The legacy
mongoshell was removed in MongoDB 6.0;mongoshreplaced it, with different output and number handling. insert(),update(),remove()andcount()are deprecated inmongosh; useinsertOne/insertMany,updateOne/updateMany/replaceOne,deleteOne/deleteManyandcountDocuments/estimatedDocumentCount.save()does not exist inmongosh, andupdate()without operators now fails instead of replacing the document.mapReduceis deprecated; use the aggregation pipeline.$whereand$functionwere un-deprecated in 9.0 but remain slower.- MMAPv1 was removed in 4.2; WiredTiger, the default since 3.2, gives document-level concurrency, compression and journaling.
- Remove callbacks, obsolete connection options and
backgroundindex options from old application code.
Exercise
Modernise this legacy script so it runs in mongosh without warnings and with the same intent:
db.products.insert([{ _id: 113, name: 'Phone Tripod', price: 699 }, { _id: 114, name: 'Laptop Sleeve', price: 799 }]); db.products.update({ category: 'Accessories' }, { $inc: { stock: 5 } }, { multi: true }); db.products.remove({ stock: 0 }); print(db.products.find({ price: { $lt: 1000 } }).count());
Show solution
db.products.insertMany([
{ _id: 113, name: 'Phone Tripod', price: 699 },
{ _id: 114, name: 'Laptop Sleeve', price: 799 }
])
db.products.updateMany({ category: 'Accessories' }, { $inc: { stock: 5 } })
db.products.deleteMany({ stock: 0 })
print(db.products.countDocuments({ price: { $lt: 1000 } }))insert() with an array becomes insertMany(), update() with multi: true becomes updateMany(), and remove() without justOne deleted every match, so it becomes deleteMany(). The cursor count() becomes countDocuments() with the same filter. On fresh data the final count is 5: the three cheap products plus the two new ones, since the accessories restock means no product has zero stock left to delete.
Quick Quiz
1. Which method replaces the deprecated count() when you need an exact number of matches?
countDocuments() counts matching documents; estimatedDocumentCount() reads fast but approximate metadata.
2. In the legacy shell, what did db.products.update({ _id: 104 }, { price: 249 }) do?
A plain document without operators replaced the document in the legacy update().
3. When was the MMAPv1 storage engine removed?
MMAPv1 was deprecated in 4.0 and removed in 4.2; WiredTiger had been the default since 3.2.
4. What is the modern replacement for most mapReduce jobs?
Aggregation is faster, uses indexes and replaced mapReduce, which is deprecated.
Learn MongoDB with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
