17

Is it possible to set multiple properties on a (sub)document in one go with Mongoose? An example of what I'm trying to do:

Let's say I have this schema:

var subSchema = new Schema({
    someField: String,
    someOtherField: String
});

var parentSchema = new Schema({
    fieldOne: String,
    subDocs: [subSchema]
})

Then I would like to do:

exports.updateMyDocument = function(req, res) {
    var parentDoc = req.parentDoc; // The parent document. Set by parameter resolver.
    var document = req.myDoc; // Sub document of parent. Set by parameter resolver.
    var partialUpdate = req.body; // updated fields sent as json and parsed by body parser
    // I know that the statement below doesn't work, it's just an example of what I would like to do.
    // Updating only the fields supplied in "partialUpdate" on the document
    document.update(partialUpdate); 
    parentDoc.save(function(err) {
        if(err) {
            res.send(500);
            return;
        }
        res.send(204);
    }); 
};

Normally, I could achieve this using the $set operator, but my problem is that document in this example is a subdocument (embedded schema) of parentDoc. So when I tried to do

Parent.update({_id: parentDoc._id, "subDocs._id": document._id}, 
    {$set: {"subDocs.$" : partialUpdate}}, 
    function(err, numAffected) {});

it replaced the subdocument instance identified by subDocs._id. Currently I have "solved" it by setting only fields manually, but I was hoping for a better way to do this.

NilsH
  • 13,705
  • 4
  • 41
  • 59

4 Answers4

38

Build up a $set object programmatically based on the fields of partialUpdate to update just those fields using dot notation:

var set = {};
for (var field in partialUpdate) {
  set['subDocs.$.' + field] = partialUpdate[field];
}
Parent.update({_id: parentDoc._id, "subDocs._id": document._id}, 
    {$set: set}, 
    function(err, numAffected) {});
JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • Yes, I did do that first. But I was hoping I didn't have to do that. – NilsH Apr 08 '13 at 14:18
  • Accepting this, since it appears to be the only way to do it. In this case, is there any need for `hasOwnProperty` check, or is this not needed since it's a plain JSON object from the request body? – NilsH Apr 09 '13 at 07:36
  • A slightly better way to loop the properties might be to use `Object.keys`, since it will only include "own properties" (although there probably won't be any other properties). – NilsH Apr 09 '13 at 11:36
  • @NilsH It wouldn't hurt, but it's not needed if it's a simple JS object built from the request. What you should think about though is whether you need to validate what fields you are including rather than blindly accepting the whole object. – JohnnyHK Apr 09 '13 at 11:37
  • Yes, that's implemented as schema validations right now. So in pair with input escaping, I hope that should be covered :) – NilsH Apr 09 '13 at 11:42
  • @NilsH Mongoose's validation is much more limited with `update` calls than it is with `save`, so keep that in mind. – JohnnyHK Apr 09 '13 at 11:53
  • Wondering how multiple nested document can be handled...Parent, children, grandchildren type of setup? – Ram Iyer Jun 01 '13 at 01:32
  • I had no luck with my grandchildren in mongoose. Support is very limited, you'll have to loop through them manually. I restructured my schema to have only one level of nesting. E. g. Mongoose's parent.child.id('123') is very handy and works only at the first level until now. – RobDil Jun 22 '13 at 22:44
  • 3
    From v3.9.3, update() takes 2 additional options: setDefaultsOnInsert and runValidators @see https://github.com/LearnBoost/mongoose/commit/1d8c3e96c7b11497d3325e9cf1f7ae66c9ee560e –  Oct 13 '14 at 07:38
7

I've done different, in a REST application.

First, I have this route:

router.put('/:id/:resource/:resourceId', function(req, res, next) {
    // this method is only for Array of resources.
    updateSet(req.params.id, req.params.resource, req, res, next);
});

and the updateSet() method

function updateSet(id, resource, req, res, next) {
    var data = req.body;
    var resourceId = req.params.resourceId;

    Collection.findById(id, function(err, collection) {
        if (err) {
            rest.response(req, res, err);
        } else {
            var subdoc = collection[resource].id(resourceId);

            // set the data for each key
            _.each(data, function(d, k) {
              subdoc[k] = d;
            });

            collection.save(function (err, docs) {
              rest.response(req, res, err, docs);
            });
        }
    });
}

The brilliant part is mongoose will validate the data if you define the Schema for this subdocument. This code will be valid for any resource of the document that is an Array. I'm not showing all my data for simplicity, but is a good practice to check for this situations and handle the response error properly.

Guilherme
  • 1,980
  • 22
  • 23
2

You can assign or extend embedded document.

    Doc.findOne({ _id: docId })
    .then(function (doc) {
      if (null === doc) {
        throw new Error('Document not found');
      }

      return doc.embeded.id(ObjectId(embeddedId));
    })
    .then(function(embeddedDoc) {
      if (null === embeddedDoc) {
        throw new Error('Embedded document not found');
      }

      Object.assign(embeddedDoc, updateData));
      return embeddedDoc.parent().save();
    })
    .catch(function (err) {
      //Do something
    });

And in this case you should be shure that _id is not assigning.

BigBadAlien
  • 236
  • 3
  • 8
0

I handled this in a slightly different manner without using the $set object. My approach is similar to Guilherme's but one difference is that I wrapped my method into the statics functionality so that it is easier to re-use throughout my application. Example below.

In CollectionSchema.js server model.

collectionSchema.statics.decrementsubdocScoreById = function decreasesubdoc (collectionId, subdocId, callback) {
  this.findById(collectionId, function(err, collection) {
    if (err) console.log("error finding collection");
    else {
      var subdoc = collection.subdocs.filter(function (subdoc) {
        return subdoc._id.equals(subdocId);
      })[0];

      subdoc.score -= 1;

      collection.save(callback);
    }
  });
};

In Server Controller

Collection.decrementsubdocScoreById(collectionId, subdocId, function  (err, data) {
  handleError(err);
  doStuffWith(data);
});
Erkin Djindjiev
  • 517
  • 4
  • 7