Partial update of a subdocument with nodejs/mongoo

2019-01-17 07:58发布

问题:

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.

回答1:

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


回答2:

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.



回答3:

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.



回答4:

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