The MongoDB Shell's New CRUD API
We released the latest development version of MongoDB the other week. The official announcement focuses on bug fixes, but I'm much more excited about a new feature: the mongo shell includes the new CRUD API! In addition to the old insert
, update
, and remove
, the shell now supports insertMany
, replaceOne
, and a variety of other new methods.
Why do I care about this, and why should you?
MongoDB's next-generation drivers, released this spring, include the new API for CRUD operations, but the shell did not initially follow suit. My reader Nick Milon commented that this is a step in the wrong direction: drivers are now less consistent with the shell. He pointed out, "developers switch more often between a driver and shell than drivers in different programming languages." So I proposed the feature, Christian Kvalheim coded it, and Kay Kim is updating the user's manual.
It's satisfying when a stranger's suggestion is so obviously right that we hurry to implement it.
The shell is now more consistent with the drivers than ever before. But beyond that, the new API is just better. For example, the old insert
method could take one document or many, and its return value didn't include the new ids:
> // the old insert API
> db.test.insert({_id: 1})
WriteResult({ "nInserted" : 1 })
> db.test.insert([{_id: 2}, {_id: 3}, {_id: 4}])
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 3,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
The new API better distinguishes single- and bulk-insert, and returns more useful results:
> // the new CRUD API
> db.test2.insertOne({_id: 1})
{
"acknowledged" : true,
"insertedId" : 1
}
> db.test2.insertMany([{_id: 2}, {_id: 3}, {_id: 4}])
{
"acknowledged" : true,
"insertedIds" : [ 2, 3, 4 ]
}
The old insert
method remains in the shell indefinitely, however: we know there are heaps of scripts written with the old methods and we don't plan to drop them.
On to the next operation. The shell's update
is famously ill-designed. No one can remember the order of the "upsert" and "multi" options, and defaulting "multi" to false stumped generations of new users:
> // the old update API
> db.test.update(
... {_id: 1},
... {$set: {x: 1}},
... true /* upsert */,
... false /* multi */
)
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : 1
})
The new updateOne
method is much easier to use correctly:
> // the new update API
> db.test2.updateOne(
... {_id: 1},
... {$set: {x: 1}},
... {upsert: true}
)
{
"acknowledged" : true,
"matchedCount" : 0,
"modifiedCount" : 0,
"upsertedId" : 1
}
We introduce updateMany
for multi-updates.
Another flaw in the old update
was, if you forgot the $-sign on an operator, you replaced a whole document instead of modifying it:
> // the old replace API
> db.test.update(
... {_id: 1},
... {set: {x: 1}} // OOPS!!
)
WriteResult({
"nMatched" : 1,
"nUpserted" : 0,
"nModified" : 1
})
> // document was replaced
> db.test.findOne()
{ "_id" : 1, "set" : { "x" : 1 } }
Now, replacing a document and updating it are kept distinct, preventing mistakes:
> // the new update API catches mistakes
> db.test2.updateOne(
... {_id: 1},
... {set: {x: 1}}
)
Error: the update operation document must contain atomic operators
>
> // explicitly replace with "replaceOne"
> db.test2.replaceOne(
... {_id: 1},
... {x: 1}
)
{
"acknowledged" : true,
"matchedCount" : 1,
"modifiedCount" : 1
}
> db.test2.findOne()
{ "_id" : 1, "x" : 1 }
The old remove
method is full of surprises: it defaults "multi" to true, although "multi" is false for updates:
> // the old delete API
> db.test.remove({}) // remove EVERYTHING!!
The new methods let you say clearly which you want:
> // the new delete API
> db.test2.deleteOne({})
{ "acknowledged" : true, "deletedCount" : 1 }
> db.test2.deleteMany({})
{ "acknowledged" : true, "deletedCount" : 3 }
MongoDB's findAndModify
command is powerful, and its options are impossible to learn.
> // the old findAndModify
> db.test.findAndModify({
... query: {_id: 1},
... update: {$set: {x: 1}},
... new: true
... })
{ "_id" : 1, "x" : 1 }
> db.test.findAndModify({
... query: {_id: 1},
... // REPLACE the document!
... update: {field: 'value'},
... // Return previous doc.
... new: false
... })
{ "_id" : 1, "x" : 1 }
> db.test.findAndModify({
... query: {_id: 1},
... remove: true
... })
{ "_id" : 1, "field" : "value" }
So we've split the one overloaded findAndModify
into three:
> // the new API
> db.test2.findOneAndUpdate(
... {_id: 1},
... {$set: {x: 1}},
... {returnNewDocument: true}
... )
{ "_id" : 1, "x" : 1 }
> db.test2.findOneAndReplace(
... {_id: 1},
... {field: 'value'},
... {returnNewDocument: false}
... )
{ "_id" : 1, "x" : 1 }
> db.test2.findOneAndDelete({_id: 1})
{ "_id" : 1, "field" : "value" }
This is not a complete description of the changes. The find
, findOne
, and other query methods have standardized new options while preserving compatibility with old scripts. There's also a new bulkWrite
method that's easier and more efficient than the old Bulk API. We'll have complete documentation for the new shell API when we publish the manual for MongoDB 3.2. Meanwhile, read Jeremy Mikola's article about the CRUD API, and the spec itself is quite legible, too.
Since the old insert
, update
, and remove
are so pervasive in our users' code we have no plans to drop them or make backwards-incompatible changes.
I'm so glad we took the time to implement the new CRUD API in the shell. It was a big effort building, testing, and documenting it—the diff for the initial patch alone is frightening—but it's well worth it to give the next generation of developers a consistent experience when they first learn MongoDB. Thanks again to Nick Milon for giving us the nudge.