The push operator appends the value to an array.
db.books.update(
{ _id: 1 },
{ $push: { isbns: '1239jasdf' } }
)
This will push the value of '1239jasdf' to the isbns
array on the books
document with _id
1.
Atomic Operations
A huge advantage when pushing to an array using mongoose push is that it is an atomic operation. An atomic operation means that the writes to the document won't overwrite eachother.
@wlaurance 👏 even more than perf, $push is atomic whereas read, push in app, send updated array could lose data from a concurrent write
— Dan MacTough (@danmactough) April 23, 2017
Thanks Dan for pointing this out!
Using Mongoose Push Each To Add Multiple Elements
If you have multiple elements to append to an array in mongoose it is
best to use $push
with the $each
modifier.
db.books.update(
{ _id: 1 },
{ $push: { isbns: {
$each: [
'1239jasdf',
'3919329AFSDKFSDf',
'9123942jasdfj'
],
$sort: -1
} } }
)
This will add three new isbns numbers all elements of the isbns. I also
added the $sort: 1
. This will sort the entire isbns array
elements. This could be an expensive operation depending on how many
elements are in the array.
A working example of pushing an element using mongoose push
To follow along:
- Clone https://github.com/wlaurance/mongoose-tips-and-tricks
cd mongoose-tips-and-tricks
;- Install mongodb
brew install mongodb
- Run
import.sh
to import the test data - Run
npm install
'use strict';
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const _ = require('lodash');
const async = require('async');
mongoose.connect('mongodb://localhost/test');
const Restaurant = mongoose.model('Restaurant', {
address: {
building: String,
coord: [Number],
street: String,
zipcode: String
},
borough: String,
cuisine: String,
grades: [{
date: Date,
grade: String,
score: Number
}],
name: String,
restaurant_id: String
});
function makeNewGrade() {
return {
date: new Date(),
grade: _.sample(['A','B','C', 'D']),
score: _.sample(['1', '2', '3', '4', '5'])
};
}
const numberOfGrades = process.env.NUM_GRADES || 10;
const restaurantId = '30075445';
function pushIteree(num, cb) {
let newGrade = makeNewGrade();
Restaurant.updateOne({
restaurant_id: restaurantId
}, {
$push: {
grades: newGrade
}
}).exec(cb);
}
function getSaveIteree(num, cb) {
Restaurant.findOne({
restaurant_id: restaurantId
}).exec((err, restaurant) => {
if (err) {
return cb(err);
}
if (!restaurant) {
return cb(new Error('Restaurant not found'));
}
restaurant.grades.push(makeNewGrade());
restaurant.save(cb);
});
}
var iteree;
if (process.env.PUSH) {
console.log('running with $push');
iteree = pushIteree;
} else {
console.log('running with get/save technique');
iteree = getSaveIteree;
}
async.each(_.range(numberOfGrades), iteree, (err) => {
console.log('Grades processed');
process.exit();
});
Most examples I see are a variation of the
getSaveIteree
function. The task is to save an element into an array
of a document. It performs a lookup using findOne
, Array.push
's the
element, and save
s the document back to the database.
A cleaner and more effective way is to use $push
. The pushIteree
function uses updateOne
with the $push
operation. This
implementation doesn't require any JSON parsing in the node.js process.
Almost all of the workload is put on Mongo. Mongo is better fit for high
CPU operations because it takes advantage of multiple threads. And if
setup correctly has the appropriate amount of RAM, CPU, and disk
requirements.
Mongoose Push benchmarks
{% asset mongoose-benchmark alt='Mongoose push vs get save' class='img-responsive' %}
The first two runs are using the getSaveIteree
function. The first
run happens in an acceptable amount of time. The second does not. The
problem will get worse with each run because the amount of data
transferred and parsed grows. Eventually the process runs out of memory.
The third, fourth, and fifth runs use the pushIteree
function. As you can see,
they both run in the same amount of time. The process doesn't have to
deal with the overhead of parsing the document into memory each time.