Mongoose: Model.save() doesn't save embedded arrays

Created on 10 Nov 2012  ·  26Comments  ·  Source: Automattic/mongoose

I have nested arrays of documents, and when I change them and do a .save() on the model it does not save the changes in the nested document.

Here are my models and schemas (some ommited for brevity):

var Votes = new mongoose.Schema({
    date: Date, 
    user_name: String,
    is_up: Boolean
});

var EventSchema = new mongoose.Schema({
  to_date: Date,
  from_date: Date,
  location: String,
  name: String,
  suggested_by: String,
  description: String,
  users: [EventUser],
  comments: [Comments],
  suggestions: [Event],
  votes: [Votes]
});

var Event = mongoose.model('Event', EventSchema);

Model before event.save() called:

{ 
     __v: 1,
    _id: 509e87e583ccbfa00e000004,
    description: 'Pfft',
    from_date: Sun Nov 11 2012 08:00:00 GMT-0500 (EST),
    location: 'Home',
    name: 'Whatever',
    suggested_by: 'No one',
    to_date: Sun Nov 11 2012 00:00:00 GMT-0500 (EST),
    votes: [],
    suggestions: 
     [ { users: [],
         comments: [],
         suggestions: [],
         votes: [],
         _id: 509e880883ccbfa00e000005,
         suggested_by: 'Some one',
         to_date: Sun Nov 11 2012 04:00:00 GMT-0500 (EST),
         from_date: Mon Nov 12 2012 00:00:00 GMT-0500 (EST),
         location: 'Home',
         name: 'Football',
         description: 'FOOTBALL!!' } ],
    comments: [],
    users: [] 
}

The same object with the nested votes right before event.save() is called.

{
   "__v":1,
   "_id":"509e87e583ccbfa00e000004",
   "description":"Pfft",
   "from_date":"2012-11-11T13:00:00.000Z",
   "location":"Home",
   "name":"Whatever",
   "suggested_by":"No one",
   "to_date":"2012-11-11T05:00:00.000Z",
   "votes":[ ],
   "suggestions":
      [ {
         "users":[],
         "comments":[ ],
         "suggestions":[ ],
         "votes":
            [{
               "is_up":true,
               "date":"2012-11-10T18:05:25.796Z",
               "user_name":"No one"
            }],
         "_id":"509e880883ccbfa00e000005",
         "suggested_by":"Some one",
         "to_date":"2012-11-11T09:00:00.000Z",
         "from_date":"2012-11-12T05:00:00.000Z",
         "location":"Home",
         "name":"Football",
         "description":"FOOTBALL!!"
      }],
   "comments":[],
   "users":[]
}

When event.save() is called, no error is thrown, but the nested votes schema inside of the nested events schema is not actually saved. If I use the same overall logic in the top level event object to save a vote, it does work.

When I looked through the code, briefly, it appears that .save() is suppose to be a shortcut for both saving new objects, as well as updating ones that already exist.

My hunch is Model.prototype._delta isn't going deep enough to catch all nested objects, https://github.com/LearnBoost/mongoose/blob/master/lib/model.js#L529

Most helpful comment

I see what the issue is - classic case of the first question on the mongoose FAQ.

Thanks, it was this issue in my case ! :)

model.myArray[index] = anyValue

becomes

model.myArray.set(index, anyValue)

All 26 comments

Please also include the code you are using to reproduce the issue (where you are manipulating the doc before save etc)

Sure, I posted it in a gist to avoid a wall of code here, https://gist.github.com/4055392

I included both saving a vote in the top-level event (working) as well as saving it within a nested event (not working). As you can see, they go about adding or updating a vote and then saving it in a similar manner.

Try fixing your EventSchema ref used in suggestions:

// bad
var EventSchema = new mongoose.Schema({
  suggestions: [EventSchema] <== at this time, EventSchema is undefined which is interpreted as Mixed

// instead...

var EventSchema = new mongoose.Schema;
EventSchema.add({
  suggestions: [EventSchema] <== EventSchema exists
})


closing, no response. please reopen if necessary.

I have the same issue where .save() returns a document which has updated the embedded object but it is not actually saved in mongodb. Please help me on this.

@Manojkumar91 can you provide some code that repros this? Would be very helpful :)

I've encountered this as well, here's my model:

/*jslint node:true */
"use strict";

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var measurement = new Schema({
    name: {type: String, required: true},
    instruction: {type: String, required: true}
});

var size = new Schema({
    name: {type: String, required: true},
    lengths: [String]
});

var chartSchema = new Schema({
    name: {type: String, required: true },
    diagram: {type: String, required: true },
    description: {type: String, required: true},
    totalMeasurements: {type: Number, required: true},
    totalSizes: {type: Number, required: true},
    measurements: [measurement],
    sizes: [size]
});

module.exports = mongoose.model('chart', chartSchema);

This is part of a much larger app so I'm only including the relevant controller action filter, but I can speak to the rest in a larger sense. Below is the filter code:

    module.exports = function (next) {
      var paramCounter = 1,
          outerLoopCounter = 0,
          existingSizes = this.chart.sizes.length,
          outerLoopEdge = this.chart.totalSizes,
          innerLoopCounter = 0,
          innerloopEdge = this.chart.totalMeasurements - 1,
          size = {name: null, lengths: [] };

      if (this.req.method && this.req.method === "POST") {
          for (outerLoopCounter;
                  outerLoopCounter < existingSizes;
                  outerLoopCounter = outerLoopCounter + 1) {

              this.chart.sizes[outerLoopCounter].name =
                  this.param("name_" + paramCounter);

              for (innerLoopCounter;
                      innerLoopCounter <= innerloopEdge;
                      innerLoopCounter = innerLoopCounter + 1) {
                  this.chart.sizes[outerLoopCounter].lengths[innerLoopCounter] =
                      this.param(this.chart.measurements[innerLoopCounter].name);
              }
              paramCounter = paramCounter + 1;
              innerLoopCounter = 0;
          }

        for (outerLoopCounter;
                outerLoopCounter < outerLoopEdge;
                outerLoopCounter = outerLoopCounter + 1) {
            size.name = this.param("name_" + paramCounter);
            for (innerLoopCounter;
                    innerLoopCounter < innerloopEdge;
                    innerLoopCounter = innerLoopCounter + 1) {
                size.lengths.push(
                    this.param(this.chart.measurements[innerLoopCounter].name
                               + "_" + paramCounter)
                );
            }
            this.chart.sizes.push(size);
            paramCounter = paramCounter + 1;
            innerLoopCounter = 0;
            size = { name: null, lengths: [] };
        }

        this.chart.save(function (err) {
            if (err) {
                console.log(err);
            }

            this.chart_info = "measurements for <strong>" + this.chart.name + "</strong> saved.";
            this.render("display");
        }.bind(this));
    } else {
        next();
    }
  };

In effect before running this filter a filter is called that creates a state-dependant form structure based on the 2 dimensional array of measurements and sizes, then this list of elements is parsed to provide updates. I've been testing with dummy data and the current dummy chart (taken from the console) looks like this:

> db.charts.find();
{ "_id" : ObjectId("553da6c3d3d0940a640e878c"), "name" : "Chart With Measurements", "diagram" : "http://res.cloudinary.com/harung71k/image/upload/v1430103747/nd4gipcxnykbbcpcztp9.jpg", "description" : "<p>This is a test of the new measurement methodology, it works.</p>", "totalMeasurements" : 4, "totalSizes" : 3, "sizes" : [ { "name" : "Small", "_id" : ObjectId("554183ed63c5945b73b8a8e7"), "lengths" : [ "1", "2", "3", "4" ] }, { "name" : "Medium", "_id" : ObjectId("554183ed63c5945b73b8a8e8"), "lengths" : [ "5", "6", "7", "8" ] }, { "name" : "Large", "_id" : ObjectId("554183ed63c5945b73b8a8e9"), "lengths" : [ "9", "10", "11", "12" ] } ], "measurements" : [ { "name" : "Fuzz", "instruction" : "<p>Fuzz Instructions</p>", "_id" : ObjectId("553dadd253eb9f996c68a381") }, { "name" : "Buzz", "instruction" : "<p>Buzz Instructions</p>", "_id" : ObjectId("553dadd253eb9f996c68a382") }, { "name" : "Beatles", "instruction" : "<p>Beatles Instructions</p>", "_id" : ObjectId("553dadd253eb9f996c68a383") }, { "name" : "Stones", "instruction" : "<p>Stones instructions</p>", "_id" : ObjectId("553ee7a09ff8c567004bd261") } ], "__v" : 3 }

I've tested the modified sizes array right before save, given an input of "1111" in the sizes[0].lengths[0] slot and this is showing up in the document when inspected in Node, but the save is returning exactly this same document as pre-save.

Please let me know if you need any additional details.

@crispen-smith I'm gonna need some clarification, there's a bit too much code for me to be able to grasp easily. Can you enable mongoose's debug mode with require('mongoose').set('debug', true); and post the output? That show me the queries and writes that are being sent to the server.

Yup, it's a lot of code, but at the same time... not quite enough to explain it easily.

So, running debug seems to suggest that the save doesn't even get hit, here's a save with a name change:

Mongoose: charts.findOne({ name: 'Chart With Measurements' }) { fields: undefined }  
Mongoose: charts.update({ _id: ObjectId("553da6c3d3d0940a640e878c"), __v: 3 }) { '$set': { 'sizes.0.name': 'Smaller' } } {} 

Here's running the same operation sequence with a change to a name and a value from the array of strings:

Mongoose: charts.findOne({ name: 'Chart With Measurements' }) { fields: undefined }  
Mongoose: charts.update({ _id: ObjectId("553da6c3d3d0940a640e878c"), __v: 3 }) { '$set': { 'sizes.0.name': 'Small' } } {} 

And, with only the a change to a variable inside the array:

Mongoose: charts.findOne({ name: 'Chart With Measurements' }) { fields: undefined }
(Yup, that's it... no update query)

I'm going to try to patch it by deleting the entire sub-doc each time, at least for the moment... I don't need referential integrity or the object ids for this particular purpose.

--UPDATE--
I've tried deleting and reloading, but that doesn't seem to work either. While I don't have output trace for this piece, it looks like the delete doesn't trigger a back-end operation either. Is there some sort of dirty-checking that isn't being triggered?

Any option to reopen this given my notes above?

@crispen-smith here's my basic attempt to reproduce the issue as a standalone script:

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');
var assert = require('assert');

mongoose.connect('mongodb://localhost:27017/gh1204');

var Schema = mongoose.Schema;

var measurement = new Schema({
    name: {type: String, required: true},
    instruction: {type: String, required: true}
});

var size = new Schema({
    name: {type: String, required: true},
    lengths: [String]
});

var chartSchema = new Schema({
    measurements: [measurement],
    sizes: [size]
});

var Chart = mongoose.model('gh1204', chartSchema);

Chart.create({}, function(error, chart) {
  assert.ifError(error);
  chart.sizes.push({ name: 'bacon', lengths: ['25'] });
  chart.save(function(error, chart) {
    assert.ifError(error);
    assert.equal(chart.sizes[0].lengths.length, 1);
    assert.equal(chart.sizes[0].lengths[0], '25');
    console.log('done');
    process.exit(0);
  });
});

No dice so far, it runs as expected. Can you modify the above example to demonstrate the issue that you're seeing? I haven't been able to translate your prose descriptions into code.

Sure thing, I'll look at this a little later this evening.

Give this a try?

/*jslint node:true */
"use strict";

var mongoose = require('mongoose');
mongoose.set('debug', true);
var util = require('util');
var assert = require('assert');

mongoose.connect('mongodb://localhost:27017/gh1204');

var Schema = mongoose.Schema;

var measurement = new Schema({
  name: {type: String, required: true},
  instruction: {type: String, required: true}
  });

var size = new Schema({
  name: {type: String, required: true},
  lengths: [String]
    });

var chartSchema = new Schema({
  measurements: [measurement],
  sizes: [size]
    });

var Chart = mongoose.model('gh1204', chartSchema);

Chart.create({}, function (error, chart) {
    assert.ifError(error);
    chart.sizes.push({ name: 'bacon', lengths: ['25'] });
    chart.save(function (error, chart) {
      assert.ifError(error);
     assert.equal(chart.sizes[0].lengths.length, 1);
     assert.equal(chart.sizes[0].lengths[0], '25');
     console.log('Created Index');

    chart.sizes[0].lengths[0] = "20";

  chart.save(function (error, chart) {
    assert.ifError(error);
    assert.equal(chart.sizes[0].lengths.length, 1);
    assert.equal(chart.sizes[0].lengths[0], '25');
    console.log('Created Index');
    process.exit(0);

    });
  });
});

Here's my console output in my node console:

Crispens-MacBook-Pro:mongooseTest crispensmith$ node index.js
Mongoose: gh1204.insert({ _id: ObjectId("5580d0c44d32b07971dfd281"), sizes: [], measurements: [], __v: 0 })   
Mongoose: gh1204.update({ __v: 0, _id: ObjectId("5580d0c44d32b07971dfd281") }) { '$set': { measurements: [] }, '$pushAll': { sizes: [ { name: 'bacon', _id: ObjectId("5580d0c44d32b07971dfd282"), lengths: [ '25' ] } ] }, '$inc': { __v: 1 } }  
Created Index
Mongoose: gh1204.update({ __v: 1, _id: ObjectId("5580d0c44d32b07971dfd281") }) { '$set': { measurements: [] }, '$inc': { __v: 1 } }  

/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:108
if (this.ended && !this.hasRejectListeners()) throw reason;
^
AssertionError: "20" == "25"
at EventEmitter. (/Users/crispensmith/Documents/mongooseTest/index.js:44:14)
at EventEmitter. (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:175:45)
at EventEmitter.emit (events.js:98:17)
at Promise.safeEmit (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:81:21)
at Promise.fulfill (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:94:24)
at Promise.resolve (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/lib/promise.js:113:23)
at model. (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/lib/document.js:1569:39)
at next_ (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/hooks-fixed/hooks.js:89:34)
at EventEmitter.fnWrapper (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_ modules/hooks-fixed/hooks.js:171:15)
at EventEmitter. (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_ modules/mpromise/lib/promise.js:175:45)
at EventEmitter.emit (events.js:98:17)
at Promise.safeEmit (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:81:21)
at Promise.fulfill (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:94:24)
at p1.then.then.self.isNew (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/lib/model.js:254:27)
at newTickHandler (/Users/crispensmith/Documents/mongooseTest/node_modules/mongoose/node_modules/mpromise/lib/promise.js:229:18)
at process._tickCallback (node.js:442:13)

(sorry, been having a beast of a time getting this into proper markdown, this is the best I was able to do.)
And here's the mongoDB output:

  > db.gh1204.find();
  { "_id" : ObjectId("5580d0c44d32b07971dfd281"), "sizes" : [ { "name" : "bacon", "_id" : ObjectId("5580d0c44d32b07971dfd282"), "lengths" : [ "25" ] } ], "measurements" : [ ], "__v" : 2 }

From a CRUD perspective, the essence of it is that there's no issue with the Create or Reads, put Updates are failing.

TLDR;
In my use case this means that once a set of lengths has been defined for a measurement they cannot be edited. I'm working on a fashion retail project with two use-cases that will require arrays within sub-docs. First use cases is that vanilla users will have their own profile, and should be able to maintain these. The second use-case is that the admin role can create custom charts for the listings for different product types (tops, bottoms etc.). I wouldn't normally expect the admin profile to need to edit these secondary charts but it would still be nice to have for that use case.

In theory, the current functionality actually presents a nice (accidental) immutable object, but the amount of work involved in using it as an immutable rather than just editing is non-trivial.

I see what the issue is - classic case of the first question on the mongoose FAQ. Mongoose can't track changes when you set an array index directly without something like ES6 proxies or ES7 Object.observe(). Use

chart.sizes[0].lengths.set(0, "20");

or

chart.sizes[0].lengths[0] = '20';
chart.markModified('sizes.0.lengths.0');

Okay, that does make sense. Strangely, (and this may be attributed to my search strategies) I was only able to find functions that operated 1x per save, and none of those would have fit the use case.

I see what the issue is - classic case of the first question on the mongoose FAQ.

Thanks, it was this issue in my case ! :)

model.myArray[index] = anyValue

becomes

model.myArray.set(index, anyValue)

This also was issue for me, I wish I saw this thread sooner!

If you're ever unsure of mongoose getting notified of changes, you can use

doc.markModified('propChanged')
doc.save() // works

I've solved my problem with Array.set() as well.
:+1:

Got the same error, this is my code snippet
image
And this is the mongoose log
image
This is what i get from the postman
image

Anything wrong?

++ EDIT ++

  1. Tried to use Array.set, get the same result

I think the better question is if there's anything right. You're doing an async save in the if (req.body.invite != []) block and then modifying the invited array in the resolve callback. The nRoom.invited.push() call will always happen after the 2nd save() call. Also, req.body.invite != [] will always be true because [] == [] is false in JS.

@vkarpov15 Haha Ok, thanks for the answer! 👍 Got a lot of things to learn :)

@peterkrieg Thanks!

Hi there, I am having this issue when updating deeply embedded docs, does anyone have any advice? My code is at:

https://stackoverflow.com/questions/51426326/updating-deeply-embedded-documents-with-mongodb-2-6-12

@thehme what version of mongoose are you using?

on this line, {$set: {["dT." + index +".ts." + i + ".th"]: newValue}}, the square brackets feel out of place to me. does it make any difference to use {$set: { "dT." + index +".ts." + i + ".th": newValue } } instead?

$set docs show just a string

feel free to join us on Gitter.im or Slack to talk about it in real time 👍

I'm using 5.5.11 and still having this issue. I've tried all the solutions proposed on this thread with no luck. My document layout looks like this:

{
  myObj: {
    myArr: [{ key: "value" }]
  }
}

myDoc.myObj.myArr.push({ key: "value2" });
// debugging myDoc shows new embedded doc
await myDoc.save();
// console shows new embedded doc is gone

Edit: Upgraded to latest (5.7.6) and still having the issue.

The below script works fine on Mongoose 5.7.6. Please open up a new issue and follow the issue template.

const mongoose = require('mongoose');

run().catch(err => console.log(err));

async function run() {
  await mongoose.connect('mongodb://localhost:27017/test', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });
  await mongoose.connection.dropDatabase();

  const schema = mongoose.Schema({ 
    myObj: { myArr: [{ key: String }] }
  });
  const Model = mongoose.model('Test', schema);


  await Model.create({ myObj: { myArr: [{ key: 'value' }] } });
  const myDoc = await Model.findOne();

  myDoc.myObj.myArr.push({ key: "value2" });
  await myDoc.save();

  console.log(myDoc.myObj.myArr);

  const saved = await Model.findOne();
  console.log(myDoc.myObj.myArr);
}
Was this page helpful?
0 / 5 - 0 ratings