mongoose Update with $in is not working properly - node.js

I have the following mongoose update in my JS:
var ids=[1,3];
restid=1;
db.model('rests').update({ _id: restid,'menu.id': {$in: ids}}, {$inc: {'menu.$.soldCounter': 1}}, {multi: true},function(err) {
if(err)
console.log("Error while updating sold counters: " + err.message);
});
for some reason, the update is being made on a rest document with _id=1, but only on menu.1.soldCounter and not on menu.3.soldCounter
I have tested it several times and the conclusion is that only the first ID from the ids array will get updated.
Can someone spot the problem?

It's because $ operator acts as a placeholder for the first element that matches, which stated in the guide : https://docs.mongodb.org/v2.6/reference/operator/update/positional/
Currently there is no way to update all sub document field in single operator. Here is the discussion on the request of this function : https://jira.mongodb.org/browse/SERVER-1243

actually there is a way to do that:
var indexes = [1,2,3];
var update = { $inc: {} };
for (var i = 0; i < indexes.length; ++i) {
update.$inc[`menu.${indexes[i]}.soldCounter`] = 1;
}
db.model('rests').update({ _id: rest._id }, update, function(err) {
if(err)
console.log("Error while updating sold counters: " + err.message);
});
you need to know the indexes you want to update in advanced. then it takes just one call to the DB.

Related

express/mongoose update query

I having problem wrapping my head around updating multiple values in my mongoDB using mongooseJS and ExpressJS.
Let say I submit an array of 2 or more objects from my frontend to "express routing" and there I get the req.body parameters to fetch it. My req.body looks like this:
[articles:
{ article: {
_id: '564209c66c23d5d20c37bd84',
quantity: 25,
},
{ article: {
_id: '564209c66c23d5d20c37bd83',
quantity: 51,
},
}]
I then need to loop? to find the specific article in the db to update and when that article is found I want to update the "quantity" value from the frontend to the correct article in db.
var id = [];
var body = {};
for (var i = req.body.length - 1; i >= 0; i--) {
id.push(req.body[i].article._id);
body[i] = req.body[i].article.quantity;
};
Articles.update(
{ _id: {$in: id} },
{ $set: {quantity: body[0].article.quantity} },
{multi: true},
function(err, response){
if(err)
console.log(err);
console.log(response);
});
The problem with this code is that I put in the first quantity value for all articles and I want it to be the correct one from the frontend. It feels like I'm on the right path but i pretty new to mongoDB and express so if there is a better solution or even a solution let me know.
Grahlie,
If you are having issues with queries, it's sometimes useful to test queries from the mongodb shell itself to workout the logic.
If your article documents are structured as such:
{
_id: ObjectId("564209c66c23d5d20c37bd84"),
quantity: 25
}
{
_id: ObjectId("564209c66c23d5d20c37bd83"),
quantity: 51
}
If you want to update the quantity of a unique document based on it's _id then you could so with this query.
db.articles.update(
{"_id": "564209c66c23d5d20c37bd84"},
{$set : { "quantity" : 25}}
)
If you wanted to update multiple documents with the same quantity you could use $in, but that's not what you want to do. You want to loop through your req.body array and update the quantity of each article.
So your code would be as such:
var articles = req.body;
var updateArticle = function(article) {
Articles.update(
{_id:article._id},
{$set:{ quantity: article.quantity}},
function(err, article){
...
);
}
for(var i = 0, n = articles.length; i < n; i++){
updateArticle(articles.[i].article);
}

MongoDB: atomic call on 2 collections (find + insert)

How can I atomically get the latest "rounds" record ObjectId and use that when inserting to the "deposits" collection?
This post answer says it can't be done: Is there any way to atomically update two collections in MongoDB?
Is this still true?
In process A, I want to atomically FIND the latest round id (rid) and INSERT that into deposits. The race condition is that after A finds rid, another process B might insert into rounds, so now A has an rid that isn't the latest, but is 1 behind. How can A finds rid in rounds + insert this rid into deposits (act on these 2 collections) atomically?
// GET ROUND ID (RID) OF LATEST
var rid;
db.rounds.find().limit(1).sort({$natural:-1}, function(err, latestInsertedRound){
rid = latestInsertedRound[0]._id;
print(rid, 'rid'); // if another process B inserts now, this rid is 1 behind
// INSERT INTO DEPOSITS
db.deposits.insert({uid:uid, iid:iid, rid:rid}, function(err, insertedDeposit){
print(insertedDeposit, 'insertedDeposit');
});
});
Inserting a document in Mongodb has a callback function that can be used. This callback function has a second parameter which returns the document inserted.
I tried printing the second parameter using console.log. It looks like :
{ result: { ok: 1, n: 1 },
ops:
[ { username: 'user1',
password: 'password1',
_id: 562099bae1872f58b3a22aed } ],
insertedCount: 1,
insertedIds: [ 562099bae1872f58b3a22aed ]
}
insertedIds is the array that holds the _ids of the inserted document or documents.
So you can insert your object in the second collection in the function callback of the insertion of first collection. A little confusing.
In simple terms : Insert the document in first collection. In it's callback, insert the document in the second collection.
MongoClient.connect(MONGOLAB_URI, function (err, db) {
if (err)
console.log("We have some error : " + err);
else {
db.createCollection('rounds', function (err, rounds) {
if (err) {
console.log('Error while creating rounds collection');
throw err;
}
else {
rounds.insert({ 'username' : 'user1', 'password' : 'password1' }, function(err,docsInserted){
console.log('Last document inserted id :', docsInserted.insertedIds[0]);
//inserting the document in the function callback
db.createCollection('deposits', function (err, deposits) {
if (err) {
console.log('Error while creating deposits collection');
throw err;
}
else {
//change array index according to your need
//you may be inserting multiple objects simultaneously
deposits.insert({'last_inserted_object' : docsInserted.insertedIds[0]);
console.log('inserted into deposits collection');
}
});
});
}
});
}
});
It seems it's not possible to operate atomically on 2 collections in MongoDB, as explained in this answer:
Is there any way to atomically update two collections in MongoDB?
I leave the question up because it has a slightly different focus (not 2 updates, but find+insert).

How to check if Mongo's $addToSet was a duplicate or not

I am using Mongoskin + NodeJS to add new keywords to MongoDB. I want to notify the user that the entry was a duplicate but not sure how to do this.
/*
* POST to addkeyword.
*/
router.post('/addkeyword', function(req, res) {
var db = req.db;
db.collection('users').update({email:"useremail#gmail.com"}, {'$addToSet': req.body }, function(err, result) {
if (err) throw err;
if (!err) console.log('addToSet Keyword.' );
});
});
The result does not seem to be of any use since it doesn't state if the keyword was added or not.
At least in the shell you can differentiate if the document was modified or not (see nModified).
> db.test4.update({_id:2}, {$addToSet: {tags: "xyz" }})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.test4.update({_id:2}, {$addToSet: {tags: "xyz" }})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 0 })
Update for Node
When you use collection.update(criteria, update[[, options], callback]); you can retrieve the count of records that were modified.
From the node docs
callback is the callback to be run after the records are updated. Has
two parameters, the first is an error object (if error occured), the
second is the count of records that were modified.
Another Update
It seems at least in version 1.4.3 the native Mongo Node driver is not behaving as documented. It is possible to work around using the bulk API (introduced in Mongo 2.6):
var col = db.collection('test');
// Initialize the Ordered Batch
var batch = col.initializeOrderedBulkOp();
batch.find({a: 2}).upsert().updateOne({"$addToSet": {"tags": "newTag"}});
// Execute the operations
batch.execute(function(err, result) {
if (err) throw err;
console.log("nUpserted: ", result.nUpserted);
console.log("nInserted: ", result.nInserted);
console.log("nModified: ", result.nModified); // <- will tell if a value was added or not
db.close();
});
You could use db.users.findAndModify({email:"useremail#gmail.com"},[],{'$addToSet': { bodies: req.body }},{'new':false}). Pay attention to new:false switcher, it allows you to get document before update and you could check whether array contained item before update. However, it could be problematic approach if your documents are big, because you analyze it on client side.
P.S. Your original query with $addToSet is wrong: field name is missing.
Edit: I tried to use count returned by update, but it returns 1 for me in all cases. Here is the code I used for test with MongoDB 2.6:
var MongoClient = require('mongodb').MongoClient;
MongoClient.connect('mongodb://localhost:27017/mtest', function(err, db) {
if(err) throw err;
db.collection('test').insert({_id:1,bodies:["test"]},function(err,item){
db.collection('test').update({_id:1},{$addToSet:{bodies:"test"}}, function(err,affected){
if(err) throw err;
console.log(affected); //1 in console
});
});
});
i am update a array from Collection with this JSON:
{
"<arrayname>":"<value>"
}
route.js
routes.post("/api/:id", Controller.addOne);
Controller.js
async addOne(req, res) {
//juryman id to list add
if (Object.keys(req.body).length === 1) {
console.log("Size 1");
}
await Session.findOneAndUpdate(
{ _id: req.params.id },
{ $addToSet: req.body }
)
.then(function(success) {
res.send("Successfully saved.");
})
.catch(function(error) {
res.status(404).send(error);
});
},
I have five arrays in my Collection and this changes the JSON array name-value and updates correctly, the respectively Collection array. This works only for one item.

Incorrect Subdocument Being Updated?

I've got a Schema with an array of subdocuments, I need to update just one of them. I do a findOne with the ID of the subdocument then cut down the response to just that subdocument at position 0 in the returned array.
No matter what I do, I can only get the first subdocument in the parent document to update, even when it should be the 2nd, 3rd, etc. Only the first gets updated no matter what. As far as I can tell it should be working, but I'm not a MongoDB or Mongoose expert, so I'm obviously wrong somewhere.
var template = req.params.template;
var page = req.params.page;
console.log('Template ID: ' + template);
db.Template.findOne({'pages._id': page}, {'pages.$': 1}, function (err, tmpl) {
console.log('Matched Template ID: ' + tmpl._id);
var pagePath = tmpl.pages[0].body;
if(req.body.file) {
tmpl.pages[0].background = req.body.filename;
tmpl.save(function (err, updTmpl) {
console.log(updTmpl);
if (err) console.log(err);
});
// db.Template.findOne(tmpl._id, function (err, tpl) {
// console.log('Additional Matched ID: ' + tmpl._id);
// console.log(tpl);
// tpl.pages[tmpl.pages[0].number].background = req.body.filename;
// tpl.save(function (err, updTmpl){
// if (err) console.log(err);
// });
// });
}
In the console, all of the ID's match up properly, and even when I return the updTmpl, it's saying that it's updated the proper record, even though its actually updated the first subdocument and not the one it's saying it has.
The schema just in case:
var envelopeSchema = new Schema({
background: String,
body: String
});
var pageSchema = new Schema({
background: String,
number: Number,
body: String
});
var templateSchema = new Schema({
name: { type: String, required: true, unique: true },
envelope: [envelopeSchema],
pagecount: Number,
pages: [pageSchema]
});
templateSchema.plugin(timestamps);
module.exports = mongoose.model("Template", templateSchema);
First, if you need req.body.file to be set in order for the update to execute I would recommend checking that before you run the query.
Also, is that a typo and req.body.file is supposed to be req.body.filename? I will assume it is for the example.
Additionally, and I have not done serious testing on this, but I believe your call will be more efficient if you specify your Template._id:
var template_id = req.params.template,
page_id = req.params.page;
if(req.body.filename){
db.Template.update({_id: template_id, 'pages._id': page_id},
{ $set: {'pages.$.background': req.body.filename} },
function(err, res){
if(err){
// err
} else {
// success
}
});
} else {
// return error / missing data
}
Mongoose doesn't understand documents returned with the positional projection operator. It always updates an array of subdocuments positionally, not by id. You may be interested in looking at the actual queries that mongoose is building - use mongoose.set('debug', true).
You'll have to either get the entire array, or build your own MongoDB query and go around mongoose. I would suggest the former; if pulling the entire array is going to cause performance issues, you're probably better off making each of the subdocuments a top-level document - documents that grow without bounds become problematic (at the very least because Mongo has a hard document size limit).
I'm not familiar with mongoose but the Mongo update query might be:
db.Template.update( { "pages._id": page }, { $set: { "pages.$.body" : body } } )

Mongoose - Increment with findOne

I'm working on some query with Mongoose where I need to skip and limit subdocuments, but in the same query I want to increment some field in document. Currently is my query built with chaining, because I got a lot of problem when I tried to do it just with options. This is what I have:
Model.findOne({ shorten_id: id }).select('title content comments views').slice('comments', [0, 10]).exec(function(err, db_res) { if (err) { throw err; } else { console.log(db_res); } });
I would like to increment filed 'views' for 1 when calling this query, but as I said, I tried a lot of things and it just didn't work.
You can use findOneAndUpdate to modify a document while also fetching one.
Model.findOneAndUpdate({ shorten_id: id }, { $inc: { fieldToIncrement: 1 })
.select('title content comments views')
.slice('comments', [0, 10])
.exec(function(err, db_res) {
if (err) {
throw err;
}
else {
console.log(db_res);
}
});

Resources