How to use Mongoose Push to add to an array

22 Apr 2017

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.

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:

'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 saves 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

Mongoose push vs get save

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.