I'm having a tough time coming up with a solution to update multiple documents with different values.
I have an app that makes sales for every sold item I want to reduce the quantity in my database by one.
var soldItems =
[
{
"_id": "1"
"name": "Foo",
"price": 1.09
},
{
"_id": "2",
"name": "Bar",
"price": 2.00
}
]
var ids = [];
soldItems.forEach(function(item){
ids.push(item._id);
});
I'm collecting all the ids in my soldItems array of objects.
Now I want to know how many items quantity I have in the database and then reduce the number quantity by one.
ModelItem.find({_id: {$in: ids}}, function(err, docs){
if(err) throw err;
docs.forEach(function(doc){
soldItems.forEach(function(item){
if(doc._id === item._id){
doc.quantity += -1;
}
});
});
Item.update({_id: {$in: ids}}, {$set: docs }, function(err){
if(err) throw err;
});
});
Obviously this is wrong because $set is passing in array instead of an object.
I want to know how can I reduce the quantity by one for each item in my database, but I the same time I don't want to go below 0 items in the database.
I'm sure im looking at this from the wrong angle.
Thanks.
Use the $inc operator instead, and the multi options:
Item.update({_id: {$in: ids}, quantity: {$gt: 0}}, // selection
{$inc: {quantity: -1}}, // modifications
{multi: true}); //options
Related
How would I be able to get the 2nd to latest or previous day's document collection using Mongoose?
my code to get the latest data goes as follows:
data.findOne()
.sort({"_id": -1})
.exec(function(err, data) {
if (err) {
console.log('Error getting data..');
}
if (data) {
res.json(data);
}
else {
console.log('No data found!');
}
});
This only returns the LATEST document in the collection. I instead need the one prior to the latest one, so one from a day before this one, how would I be able to do this?
Then you'd have to use aggregation:
db.getCollection('data').aggregate([
{ $sort: {"date": -1}}, // sort by latest date (descending)
{ $limit: 2}, // limit to the first two results
{ $sort: {"date": 1}}, // sort these two ascending
{ $limit: 1} // get first document
])
This pipeline is translated in mongoose like that (I think):
data.aggregate([
{ $sort: {"date": -1}},
{ $limit: 2},
{ $sort: {"date": 1}},
{ $limit: 1}
], function (err, result) {
if (err) {
next(err);
} else {
res.json(result);
}
});
Also by sorting without adding a $match pipeline, your first $sort would sort all the documents in your collection. So if your collection is big you might want to restrict them with some query parameters that you can add to the $match pipeline. You could add the match before the first $sort
E.g.
db.getCollection('data').aggregate([
{ $match: {...}},
{ $sort: {"date": -1}},
{ $limit: 2},
{ $sort: {"date": 1}},
{ $limit: 1}
])
To get the 2nd before the last
data.find().sort({ _id: -1 }).limit(1).skip(2).exec(function(err, data) {
if (err) {
console.log('Error getting data..');
}
if (data) {
res.json(data);
}
else {
console.log('No data found!');
}
});
you could do like this:
data
.find()
.limit(2)
.sort({ "_id": -1 })
.exec(function(err, data) {
if (err) return res.json(err);
return res.json(data);
});
this will find on all documents sorted by id: -1, and then limit the result to two, this should give you the result that you want.
This is the fix I used to make it work for what I wanted, hope it helps others:
I decided to import MomentJS, from there it displays the current date, subtracting 1 from it, to display the previous date's value, hence giving me the proper collection from the day before.
Code:
const moment = require('moment');
...
data.find({"updated": moment().subtract(1, 'days').format('L')}) ...
I am not sure whether the title explains what I try to say.
But here assume collection's structure is like;
[
{email: "alex#sda.com"},
{email: "elizabeth#sds.com"},
{email: "hannah#xx.com"},
]
I want to get emails but before the # character.
What have I done to achieve this?
db.collection.find({}).toArray(function(err, result){
var emails = [];
for(var i =0; i< result.length; i++){
emails.push(result[i].split("#")[0]);
}
})
But I don't find this efficient because after the query, I loop through the result and store them in new array.
Is there a better way to get only the alex, elizabeth, hannah parth with a query?
This is just an example. I want to ask your suggestions because I have lots of situations like this. (I am looking for a most efficient way to trim, update some values in the collections)
You can use the distinct method first to get the list of email addresses then use regex to get the names:
db.collection.distinct("email", function(err, result){
var emails = result.map(function(email){
return email.match(/^([^#]*)#/)[1];
});
console.log(emails);
});
With the aggregation framework, MongoDB 3.4 has a $split operator that you can use in conjunction with $arrayElemAt:
db.collection.aggregate([
{
"$group": {
"_id": null,
"emails": {
"$push": {
"$arrayElemAt": [
{ "$split": ["$email", "#"] },
0
]
}
}
}
}
], function(err, result){
if (err) throw err;
console.log(result);
/*
{
"_id" : null,
"emails" : [
"alex",
"elizabeth",
"hannah"
]
}
*/
});
I'm using nodeJS + Express + Mongoose + mongoDB
Here's my mongoDB User Schema:
{
friends: [ObjectId]
friends_count: Number
}
Whenever user adds a friend, a friendId will be pushed into friends array, and friends_count will be increased by 1.
Maybe there are a lot of actions will change the friends array, and maybe I will forgot to increase the friends_count. So I want to make sure that friends_count always equal to friends.length
Is there a good way or framework to make sure all of that?
P.S
I know how to update friends_count. What I mean is what if I forgot to?
Is there a way to automatically keep these two attributes sync?
Use the $ne operator as a "query" argument to .update() and the $inc operator to apply when that "friend" did not exist within the array as you $push the new member:
User.update(
{ "_id": docId, "friends": { "$ne": friendId } },
{
"$push": { "friends": friendId },
"$inc": { "friends_count": 1 }
},
function(err,numberAffected) {
}
)
Or to "remove" a friend from the list, do the reverse case with $pull:
User.update(
{ "_id": docId, "friends": friendId },
{
"$pull": { "friends": friendId },
"$inc": { "friends_count": -1 }
},
function(err,numberAffected) {
}
)
That way your friends_count stays in sync with the number of array elements present.
All you need to do is to update friends_count in both add and remove functions. For example:
User.findById(userId, function (err, user) {
if (user) {
user.friends.push(friendId);
user.friends_count++;
user.save();
}
});
FYI, I don't think it is necessary to add friends_count while you can get total numbers of friends by friends.length.
I want to update multiple docs with different values.
My Database looks something like this.
[
{
"_id": 1,
"value": 50
},
{
"_id": 2,
"value": 100
}
]
This Query return an error because i'm passing an array instead of an object in the $set.
Model.update({_id: {$in: ids}}, {$set: ids.value}, {multi: true};
I want my database to look like this
[
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
]
Supposing you had an array of objects that you wanted to update in your collection on matching ids like
var soldItems = [
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
];
then you could use the forEach() method on the array to iterate it and update your collection:
soldItems.forEach(function(item)){
Model.update({"_id": item._id}, {"$set": {"value": item.value }}, callback);
});
or use promises as
var updates = [];
soldItems.forEach(function(item)){
var updatePromise = Model.update({"_id": item._id}, {"$set": {"value": item.value }});
updates.push(updatePromise);
});
Promise.all(updates).then(function(results){
console.log(results);
});
or using map()
var updates = soldItems.map(function(item)){
return Model.update({"_id": item._id}, {"$set": {"value": item.value }});
});
Promise.all(updates).then(function(results){
console.log(results);
});
For larger arrays, you could take advantage of using a bulk write API for better performance. For Mongoose versions >=4.3.0 which support MongoDB Server 3.2.x,
you can use bulkWrite() for updates. The following example shows how you can go about this:
var bulkUpdateCallback = function(err, r){
console.log(r.matchedCount);
console.log(r.modifiedCount);
}
// Initialise the bulk operations array
var bulkOps = soldItems.map(function (item) {
return {
"updateOne": {
"filter": { "_id": item._id } ,
"update": { "$set": { "value": item.value } }
}
}
});
// Get the underlying collection via the native node.js driver collection object
Model.collection.bulkWrite(bulkOps, { "ordered": true, w: 1 }, bulkUpdateCallback);
For Mongoose versions ~3.8.8, ~3.8.22, 4.x which support MongoDB Server >=2.6.x, you could use the Bulk API as follows
var bulk = Model.collection.initializeOrderedBulkOp(),
counter = 0;
soldItems.forEach(function(item) {
bulk.find({ "_id": item._id }).updateOne({
"$set": { "value": item.value }
});
counter++;
if (counter % 500 == 0) {
bulk.execute(function(err, r) {
// do something with the result
bulk = Model.collection.initializeOrderedBulkOp();
counter = 0;
});
}
});
// Catch any docs in the queue under or over the 500's
if (counter > 0) {
bulk.execute(function(err,result) {
// do something with the result here
});
}
First of all your update() query is not ok..
See documentation for this here
Model.update(conditions, update, options, callback);
Suppose your current db model contains docs like this (as you described in question as well):
[
{
"_id": 1,
"value": 50
},
{
"_id": 2,
"value": 100
}
];
and you've below array which contains objects (i.e., docs) to be modified with current db's docs to like this:
idsArray: [
{
"_id": 1,
"value": 4
},
{
"_id": 2,
"value": 27
}
];
From my experience with mongodb & mongoose, i don't think you can update all docs with single line query (that's you're trying to do).. (P.S. I don't know about that so I am not sure to this..)
But to make your code work, you will be doing something like this:
Idea: Loop over each doc in docs i.e, idsArray and call update() over it..
So, Here's code to this:
idsArray.forEach(function(obj) {
Model.update({_id: obj._id}, {$set: {value: obj.value}});
});
In above code, I am supposing you've _id values in db docs as they 're written above (i.e, "_id": 1).. But if they're like this "_id": ObjectId('1')
[
{
"_id": ObjectId('1'),
"value": 50
},
.....
.....
]
then you'll be need to convert _id to ObjectId(obj._id) in update() query..
so for that you'll be doing like this.
var ObjectId = require('mongodb').ObjectID;
idsArray.forEach(function(obj) {
Model.update({_id: ObjectId(obj._id)}, {$set: {value: obj.value}});
});
P.S. Just confirm it (i.e., _id) before go forward..
Hope this helps.
Cheers,
Multi update can be used only for updating multiple documents to the same value(s) or updating with the same update operation for all documents.
If you want to update to different values you have to use several update statements.
how can i combine match document's subdocument together as one and return it as an array of object ? i have tried $group but don't seem to work.
my query ( this return array of object in this case there are two )
User.find({
'business_details.business_location': {
$near: coords,
$maxDistance: maxDistance
},
'deal_details.deals_expired_date': {
$gte: new Date()
}
}, {
'deal_details': 1
}).limit(limit).exec(function(err, locations) {
if (err) {
return res.status(500).json(err)
}
console.log(locations)
the console.log(locations) result
// give me the result below
[{
_id: 55 c0b8c62fd875a93c8ff7ea, // first document
deal_details: [{
deals_location: '101.6833,3.1333',
deals_price: 12.12 // 1st deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.3 // 2nd deal
}],
business_details: {}
}, {
_id: 55 a79898e0268bc40e62cd3a, // second document
deal_details: [{
deals_location: '101.6833,3.1333',
deals_price: 12.12 // 3rd deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.78 // 4th deal
}, {
deals_location: '101.6833,3.1333',
deals_price: 34.32 // 5th deal
}],
business_details: {}
}]
what i wanted to do is to combine these both deal_details field together and return it as an array of object. It will contain 5 deals in one array of object instead of two separated array of objects.
i have try to do it in my backend (nodejs) by using concat or push, however when there's more than 2 match document i'm having problem to concat them together, is there any way to combine all match documents and return it as one ? like what i mentioned above ?
What you are probably missing here is the $unwind pipeline stage, which is what you typically use to "de-normalize" array content, particularly when your grouping operation intends to work across documents in your query result:
User.aggregate(
[
// Your basic query conditions
{ "$match": {
"business_details.business_location": {
"$near": coords,
"$maxDistance": maxDistance
},
"deal_details.deals_expired_date": {
"$gte": new Date()
}},
// Limit query results here
{ "$limit": limit },
// Unwind the array
{ "$unwind": "$deal_details" },
// Group on the common location
{ "$group": {
"_id": "$deal_details.deals_location",
"prices": {
"$push": "$deal_details.deals_price"
}
}}
],
function(err,results) {
if (err) throw err;
console.log(JSON.stringify(results,undefined,2));
}
);
Which gives output like:
{
"_id": "101.6833,3.1333",
"prices": [
12.12,
34.3,
12.12,
34.78,
34.32
]
}
Depending on how many documents actually match the grouping.
Alternately, you might want to look at the $geoNear pipeline stage, which gives a bit more control, especially when dealing with content in arrays.
Also beware that with "location" data in an array, only the "nearest" result is being considered here and not "all" of the array content. So other items in the array may not be actually "near" the queried point. That is more of a design consideration though as any query operation you do will need to consider this.
You can merge them with reduce:
locations = locations.reduce(function(prev, location){
previous = prev.concat(location.deal_details)
return previous
},[])