Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model.save() doesn't save embedded arrays #1204

Closed
ograycode opened this issue Nov 10, 2012 · 26 comments
Closed

Model.save() doesn't save embedded arrays #1204

ograycode opened this issue Nov 10, 2012 · 26 comments

Comments

@ograycode
Copy link

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

@aheckmann
Copy link
Collaborator

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

@ograycode
Copy link
Author

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.

@aheckmann
Copy link
Collaborator

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
})

@aheckmann
Copy link
Collaborator

closing, no response. please reopen if necessary.

@Manojkumar91
Copy link

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.

@vkarpov15
Copy link
Collaborator

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

@crispen-smith
Copy link

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.

@vkarpov15
Copy link
Collaborator

@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.

@crispen-smith
Copy link

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?

@crispen-smith
Copy link

Any option to reopen this given my notes above?

@vkarpov15 vkarpov15 reopened this May 23, 2015
@vkarpov15 vkarpov15 added this to the 3.8.30 milestone May 23, 2015
@vkarpov15 vkarpov15 modified the milestones: 3.8.31, 3.8.30 Jun 5, 2015
@vkarpov15
Copy link
Collaborator

@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.

@vkarpov15 vkarpov15 added the can't reproduce Mongoose devs have been unable to reproduce this issue. Close after 14 days of inactivity. label Jun 16, 2015
@crispen-smith
Copy link

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

@crispen-smith
Copy link

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.

@vkarpov15 vkarpov15 removed the can't reproduce Mongoose devs have been unable to reproduce this issue. Close after 14 days of inactivity. label Jun 17, 2015
@vkarpov15
Copy link
Collaborator

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');

@vkarpov15 vkarpov15 removed this from the 3.8.31 milestone Jun 20, 2015
@crispen-smith
Copy link

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.

@4lador
Copy link

4lador commented Jan 27, 2016

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)

@peterkrieg
Copy link

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

@allanesquina
Copy link

I've solved my problem with Array.set() as well.
👍

@kenanchristian
Copy link

kenanchristian commented Apr 13, 2017

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

@vkarpov15
Copy link
Collaborator

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.

@kenanchristian
Copy link

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

@virlit
Copy link

virlit commented Mar 22, 2018

@peterkrieg Thanks!

@thehme
Copy link

thehme commented Jul 19, 2018

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

@lineus
Copy link
Collaborator

lineus commented Jul 19, 2018

@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 👍

@shivpatel
Copy link

shivpatel commented Oct 24, 2019

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.

@vkarpov15 vkarpov15 reopened this Nov 2, 2019
@vkarpov15 vkarpov15 added the needs repro script Maybe a bug, but no repro script. The issue reporter should create a script that demos the issue label Nov 2, 2019
@vkarpov15 vkarpov15 modified the milestones: 5.7.8, 5.7.9 Nov 2, 2019
@vkarpov15
Copy link
Collaborator

vkarpov15 commented Nov 8, 2019

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);
}

@vkarpov15 vkarpov15 removed this from the 5.7.9 milestone Nov 8, 2019
@vkarpov15 vkarpov15 removed the needs repro script Maybe a bug, but no repro script. The issue reporter should create a script that demos the issue label Nov 8, 2019
@Automattic Automattic locked as resolved and limited conversation to collaborators Nov 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests