4

I am trying to update a MongoDB document with the following code:

exports.update = function (req, res) {
  if (req.body._id) { delete req.body._id; }
  Product.findById(req.params.id, function (err, product) {
      if (err) { return handleError(res, err); }
      if (!product) { return res.send(404); }
      var updated = _.merge(product, req.body);
      updated.save(function (err) {
          if (err) { return handleError(res, err); }
          return res.status(200).json(product);
      });
  });
};

The code executes successfully, but the existing database array values are not updated by the .save. The content of req.body is as follows (of particular note are the values in the "start" array):

{
    "_id" : ObjectId("563a95d9cc2d38622b867ecf"),
    "productName" : "Product Name",
    "productVersion" : "1",
    "productOverview" : "Description of product.",
    "productManager" : ObjectId("563a90de195e72712a197d06"),
    "businessPriority" : "1 Must Do",
    "businessRank" : 2,
    "businessFactors" : {
        "growth" : true,
        "diversification" : true,
        "architecture" : false,
        "riskMitigation" : false,
        "retention" : false
    },
    "complete" : false,
    "phase" : "Discovery",
    "comment" : [
        "Discovery phase comments",
        "Development phase comments",
        "Pilot phase comments",
        "Pre-launch phase comments",
        "Post-launch phase comments"
    ],
    "finish" : [
        "2015-11-30",
        "2016-03-31",
        "2016-05-31",
        "2016-06-30",
        "2016-08-31"
    ],
    "start" : [
        "2015-07-01",
        "2015-12-01",
        "2016-04-01",
        "2016-06-01",
        "2016-07-01"
    ]
}

The .findById successfully retrieves the existing document out of the database, which contains only the "start" array:

{
    "_id" : ObjectId("563a95d9cc2d38622b867ecf"),
    "start" : [
        "07-02",
        "12-01",
        "04-01",
        "06-01",
        "07-01"
    ]
}

The lodash .merge function construct a correct "updated" record (which has the same data content as req.body above).

The .save executes without error, and a 200 status is returned. However, the content of the document in the database still contains the original data for the "start" element:

{
    "_id" : ObjectId("563a95d9cc2d38622b867ecf"),
    "start" : [
        "07-02",
        "12-01",
        "04-01",
        "06-01",
        "07-01"
    ],
    "businessFactors" : {
        "growth" : true,
        "diversification" : true
    },
    "businessPriority" : "1 Must Do",
    "businessRank" : 2,
    "comment" : [
        "Discovery phase comments",
        "Development phase comments.",
        "Pilot phase comments",
        "Pre-launch phase comments",
        "Post-launch phase comments"
    ],
    "finish" : [
        "2015-11-30",
        "2016-03-31",
        "2016-05-31",
        "2016-06-30",
        "2016-08-31"
    ],
    "phase" : "Discovery",
    "productManager" : ObjectId("563a90de195e72712a197d06"),
    "productName" : "New Product",
    "productOverview" : "Description of product.",
    "productVersion" : "1",
    "__v" : 1
}

The Mongoose Schema is as follows:

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

var productSchema = new Schema(
    {
        productName             : String,
        productVersion          : String,
        productOverview         : String,
        productManager          : Schema.Types.ObjectId,
        businessPriority        : String,
        businessRank            : Number,
        businessFactors         : {
            retention           : Boolean,
            growth              : Boolean,
            diversification     : Boolean,
            architecture        : Boolean,
            riskMitigation      : Boolean
        },
        start                   : [ String ],
        finish                  : [ String ],
        comment                 : [ String ],
        phase                   : String,
        complete                : Boolean
    },
    {
        collection              : 'products'
    }
);

module.exports = mongoose.model('Product', productSchema);

Any guidance on what might be happening here? I am using MongoDb version 3.0.6 and Mongoose version 4.1.12 on NodeJS version 4.1.1 and Express version 4.13.3.

Mike McBride
  • 43
  • 1
  • 4

2 Answers2

3

you could findOneAndUpdate instead of finding the id first then saving. if the id isn't there it will create a new one. If you don't want it to save a new one set upsert to false

Product.findOneAndUpdate({_id:<your id>, {$set: <your merged JSON>}, {upsert:true}, function(err, effected, raw){});
Nick Red
  • 396
  • 3
  • 17
  • A little side note. Using *findOneAndUpdate* will not return the updated the document but the original document. – Thomas Bormans Nov 05 '15 at 08:23
  • I'll try this as a workaround. It seems that the values that are not updating correctly are arrays (primitives seem to update just fine). – Mike McBride Nov 05 '15 at 16:03
  • This seems to have solved the problem, the values are now getting posted to the database; thanks for the suggestion. Still a bit curious as to why the original method did not work, but I can live with the alternative. As far as Thomas's comment about the value returned, that isn't an impact in my app, as I'm done with the document once it is written ... but it did confuse me a bit when debugging the code!! Thanks for the assistance. – Mike McBride Nov 05 '15 at 18:33
0

Try using _.extend or _.assign instead of _.merge:

var updated = _.assign(product, req.body);

This answer by ShitalShah highlights the differences between merge and extend:

Here's how extend/assign works: For each property in source, copy its value as-is to destination. if property values themselves are objects, there is no recursive traversal of their properties. Entire object would be taken from source and set in to destination.

Here's how merge works: For each property in source, check if that property is object itself. If it is then go down recursively and try to map child object properties from source to destination. So essentially we merge object hierarchy from source to destination. While for extend/assign, it's simple one level copy of properties from source to destination.

JSBin to illustrate the differences.

exports.update = function (req, res) {
  if (req.body._id) { delete req.body._id; }
  Product.findById(req.params.id, function (err, product) {
      if (err) { return handleError(res, err); }
      if (!product) { return res.send(404); }
      var updated = _.assign(product, req.body);
      updated.save(function (err) {
          if (err) { return handleError(res, err); }
          return res.status(200).json(product);
      });
  });
};

Check the demo below.

var dest = {
 foo : {
  b1 : "b1 value",
  b2 : "b2 value"
 },
 baz : {
  q1 : "q1 value"
 },
 mofo : "mofo value"
};

var src = { 
 foo : { 
  b1: "overwritten b1", 
  b3: "b3 value"
 },
 mofo : "overwritten mofo"
};


var assigned = _.clone(dest);
_.assign(assigned,src);
console.log("assign:", assigned);

var merged = _.clone(dest);
_.merge(merged,src);
console.log("merge:", merged);

var defaulted = _.clone(dest);
_.defaults(defaulted,src);
console.log("defaults:", defaulted);

pre.innerHTML = "assign: " + JSON.stringify(assigned, null, 4) + "</br>merge: " + JSON.stringify(merged, null, 4) + "</br>defaults: "+ JSON.stringify(defaulted, null, 4);
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<pre id="pre"></pre>
Community
  • 1
  • 1
chridam
  • 100,957
  • 23
  • 236
  • 235
  • Thanks for the suggestion. The code segment initially came from a framework that I had used on a prior project, so I didn't modify their approach to consolidating the two objects. At any rate, the "updated" object has the correct content, it just is not making it back into the database following the 'save'. I don't see how a change to the lodash consolidation approach would address that, but I could be missing the point. – Mike McBride Nov 05 '15 at 16:02
  • 1
    Shital, this is informative. I probably could use an .assign in this particular case, but there are other operations for which the .merge is the right approach (where I'm sending only updated parameters) and they all use this same interface. I was a bit surprised by the behavior of the .default operation, will have to contemplate that one for a bit. Appreciate the input! – Mike McBride Nov 05 '15 at 18:38