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