How to update array of object in robomongo [duplicate] - node.js

Assume we have the following collection, which I have few questions about:
{
"_id" : ObjectId("4faaba123412d654fe83hg876"),
"user_id" : 123456,
"total" : 100,
"items" : [
{
"item_name" : "my_item_one",
"price" : 20
},
{
"item_name" : "my_item_two",
"price" : 50
},
{
"item_name" : "my_item_three",
"price" : 30
}
]
}
I want to increase the price for "item_name":"my_item_two" and if it doesn't exists, it should be appended to the "items" array.
How can I update two fields at the same time? For example, increase the price for "my_item_three" and at the same time increase the "total" (with the same value).
I prefer to do this on the MongoDB side, otherwise I have to load the document in client-side (Python) and construct the updated document and replace it with the existing one in MongoDB.
This is what I have tried and works fine if the object exists:
db.test_invoice.update({user_id : 123456 , "items.item_name":"my_item_one"} , {$inc: {"items.$.price": 10}})
However, if the key doesn't exist, it does nothing.
Also, it only updates the nested object. There is no way with this command to update the "total" field as well.

For question #1, let's break it into two parts. First, increment any document that has "items.item_name" equal to "my_item_two". For this you'll have to use the positional "$" operator. Something like:
db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } ,
{$inc : {"items.$.price" : 1} } ,
false ,
true);
Note that this will only increment the first matched subdocument in any array (so if you have another document in the array with "item_name" equal to "my_item_two", it won't get incremented). But this might be what you want.
The second part is trickier. We can push a new item to an array without a "my_item_two" as follows:
db.bar.update( {user_id : 123456, "items.item_name" : {$ne : "my_item_two" }} ,
{$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
false ,
true);
For your question #2, the answer is easier. To increment the total and the price of item_three in any document that contains "my_item_three," you can use the $inc operator on multiple fields at the same time. Something like:
db.bar.update( {"items.item_name" : {$ne : "my_item_three" }} ,
{$inc : {total : 1 , "items.$.price" : 1}} ,
false ,
true);

There is no way to do this in single query. You have to search the document in first query:
If document exists:
db.bar.update( {user_id : 123456 , "items.item_name" : "my_item_two" } ,
{$inc : {"items.$.price" : 1} } ,
false ,
true);
Else
db.bar.update( {user_id : 123456 } ,
{$addToSet : {"items" : {'item_name' : "my_item_two" , 'price' : 1 }} } ,
false ,
true);
No need to add condition {$ne : "my_item_two" }.
Also in multithreaded enviourment you have to be careful that only one thread can execute the second (insert case, if document did not found) at a time, otherwise duplicate embed documents will be inserted.

We can use $set operator to update the nested array inside object filed update the value
db.getCollection('geolocations').update(
{
"_id" : ObjectId("5bd3013ac714ea4959f80115"),
"geolocation.country" : "United States of America"
},
{ $set:
{
"geolocation.$.country" : "USA"
}
},
false,
true
);

One way to ensure there are no duplicates of the "item_name" fields would be the do the same actions as given in the answer https://stackoverflow.com/a/10523963 for Question #1 but in reverse order!
Push into the document if "items.item_name":{"$ne":"my_name"} - the update filter MUST contain some uniquely indexed field! And upsert is false.
Increment the document if "items.item_name":"my_name".
The first update should be atomic, thus it doesn't do anything if the array already contains the element with the item_name "my_name".
By the time the second update happens, there must be an array element with the "item_name"="my_name"

Related

add field to all documents in mongodb collection

i am working on node js project, i have a mongodb collection of orders in which i want to add a field to every document , the problem is that i want to pass a function as the value of this new field and the arguments of the function are two other fields of the document , this is an example that will make you understand .
my collection :
[
{
_id:"eyxwapfhiezfe664ec",
orderPrice : 20,
createdAt : 2021-01-15T17:16:25.844Z
endedAt : 2021-01-15T17:20:25.844Z
}
{
_id:"eyxwlcfeojrfeoc",
orderPrice : 50,
createdAt : 2021-01-15T17:16:25.844Z
endedAt : 2021-01-15T17:20:25.844Z
}
{
_id:"eyxwapfhiseflflpsssc",
orderPrice : 20,
createdAt : 2021-01-15T17:16:25.844Z
endedAt : 2021-01-15T17:20:25.844Z
}
the field i want to add is completedTime which is the difference between createdAt and endedAt i have a function differenceBetweenDates which takes two dates as argument createdAt and ended at but i don't know how to pass this function to $set in update .
So to be more clear i want to add this field to every document :
CompletedTime: differenceBetweenDates(createdAt, endedAt)
thank you .
If you are using MongoDB 4.2, you can pass aggregation expressions as part of the update.
db.collection.updateMany({},[{ $set:{ CompletedTime: {$subtract:["$endedAt", "$createdAt" ]}}}])
In MongoDB 4.4 you might also use an aggregation pipeline with a $merge stage to merge the result set back into the original collection:
db.collection.aggregate([
{ $set: { CompletedTime: {$subtract:["$endedAt", "$createdAt" ]}}},
{ $merge: "collection" }
])

complicated mongoose pull list of data from api and insert into mongodb if it doesn't already exist

I am connecting to the Yelp API using the RapidAPI module in Nodejs. I am able to request a token, connect, and request data, retrieve that data, and insert the relevant information for each result it into mongodb. Here's where it gets complicated...
Let's say I make a Yelp API request and search for bars. I get a list of bars and insert them into the database. Let's say one of these in the list is "Joe's Bar & Grill". One of the fields in my mongodb is "type" and it's an array. So now, this particular document will look something like this:
{
id: 'joes-bar-and-grill',
name: 'Joe\'s Bar & Grill',
type: ['bar']
}
But then I run another request on the Yelp API on "restaurants", and in this list "Joe's Bar & Grill" shows up again. Instead of inserting a new duplicate document into mongodb, I'd like the existing document to end up looking like this:
{
id: 'joes-bar-and-grill',
name: 'Joe\'s Bar & Grill',
type: ['bar', 'restaurant']
}
In addition to this, let's say I run another request again for "bars", and "Joe's Bar & Grill" comes up again. I don't want it to automatically insert "bar" into the type array again, if "bar" already exists in its array.
I've tried findOneAndUpdate with upsert: true and a $push of new data into the array, but I cannot get it to work at all. Does anyone have any ideas?
You can use findOneAndUpdate, combined with $addToSet (to make sure that an entry in the array only exists once) and $each (to allow passing arrays to $addToSet):
Bar.findOneAndUpdate({ id : 'joes-bar-and-grill' }, {
id : 'joes-bar-and-grill',
name : 'Joe\'s Bar & Grill',
$addToSet : { type : { $each : [ 'restaurant' ] } }
}, { upsert : true })
EDIT: now that you posted your entire code, the problem becomes more obvious.
For one, I'm not sure if the third and fourth arguments that you're passing to Location.update() make sense. As far as I know, the third should be an option object, and the fourth an async function.
Secondly, it looks like you're just ignoring any update errors.
And lastly, this isn't going to work:
for (var i = 0; i < payload.businesses.length; i++) { Location.update(...) }
Because Location.update() is asynchronous, the i variable will get clobbered (you should browse around on SO to find the explanation for that; for example, see this question).
You're going to need a library that will provide you with better async support, and preferably one that will also help limiting the number of update queries.
Once such library is async, and using it, your code would become something like this:
const async = require('async');
...
async.eachLimit(payload.businesses, 5, function(business, callback) {
Location.update({ yelpID : business.id }, {
name : business.name,
latitude : business.location.latitude,
longitude : business.location.longitude,
address1 : business.location.address1,
address2 : business.location.address2,
address3 : business.location.address3,
city : business.location.city,
state : business.location.state,
zip_code : business.location.zip_code,
country : business.location.country,
timezone : 'CST'
$addToSet : { type : 'bar' }
}, { upsert : true }, callback);
}, function(err) {
if (err) {
console.error(err);
} else {
console.log('All documents inserted');
}
});
You may use $addToSet operator
The $addToSet operator adds a value to an array unless the value is
already present, in which case $addToSet does nothing to that array.
$addToSet only ensures that there are no duplicate items added to the
set and does not affect existing duplicate elements. $addToSet does
not guarantee a particular ordering of elements in the modified set.
If the field is absent in the document to update, $addToSet creates
the array field with the specified value as its element.
If the field is not an array, the operation will fail.
The below solution assumes that on each update, you receive a single type and not an array. If the input document is an array itself, you may use robertklep's solution with $each operator
db.mycoll.update(
{ "id" : "joes-bar-and-grill" },
{
$set:{
name : 'Joe\'s Bar & Grill',
},
$addToSet : { type : 'restaurant' }
},
true, false);
I have also used $set operator.
The $set operator replaces the value of a field with the specified
value.
The $set operator expression has the following form:
{ $set: { field1: value1, ... } }
Here is the mongo shell output to explain it further :
> db.mycoll.find({ "id" : "joes-bar-and-grill" });
// NO RESULT
> db.mycoll.update(
... { "id" : "joes-bar-and-grill" },
... {
... $set:{
... name : 'Joe\'s Bar & Grill',
... },
... $addToSet : { type : 'restaurant' }
... },
... true, false);
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("58e719b4d543c5e30d615d59")
})
// INSERTED A NEW DOCUMENT AS IT DOES NOT EXIST
> db.mycoll.find({ "id" : "joes-bar-and-grill" }); // FINDING THE OBJECT
{ "_id" : ObjectId("58e719b4d543c5e30d615d59"), "id" : "joes-bar-and-grill", "name" : "Joe's Bar & Grill", "type" : [ "restaurant" ] }
> db.mycoll.update(
... { "id" : "joes-bar-and-grill" },
... {
... $set:{
... name : 'Joe\'s Bar & Grill',
... },
... $addToSet : { type : 'bar' }
... },
... true, false);
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
// UPDATING THE DOCUMENT WITH NEW TYPE : "bar"
> db.mycoll.findOne({ "id" : "joes-bar-and-grill" });
{
"_id" : ObjectId("58e719b4d543c5e30d615d59"),
"id" : "joes-bar-and-grill",
"name" : "Joe's Bar & Grill",
"type" : [
"restaurant",
"bar"
]
}

Mongoose: When you $pull an element from a sub-array, can you remove that sub-array item if it is now empty?

If I $pull the last member out of the members array, can I then remove that room? Given the document below, I would want rooms to end up being an empty array.
var query = {game:'/donut_fun', 'rooms.roomname': 'Joey\'s Room'};
var update = {$pull: {'rooms.$.members': player}};
GamesModel.findOneAndUpdate(query, update, function(err, doc){...
And here's my resulting document after that update. Note that the room no longer has any members so I want rooms to just be an empty array now. I don't care about Joey's Room anymore.
{
"_id" : ObjectId("588e4d64c23324935a061123"),
"game" : "/donut_fun",
"__v" : 0,
"rooms" : [
{
"members" : [ ],
"timestamp" : 1485720932863,
"roomname" : "Joey's Room"
}
]
}

Insert an array item after a specific value in mongodb

I have db like;
{"_id" : 1 , children: ["2", "123", "5"]}
I want to insert an element into children array. It is easy when you know the index. $position can be used for this purpose. Like;
db.module_menu.update({"_id" : "1"} ,
{$push : {"children" : {
$each : ["12"] , $position : 1}}});
But lets say I don't know the position index, I know the value of element that I want to insert after.
I have value: 12 and value that I want to insert after. insertAfter: 2
As a result it should be;
{"_id" : 1, text: "" , children: ["2","12", "123", "5"]}
How can I do it?
Go through each element in the children array find the index of value and by using $position, insert the value. Is it the only way to do it?
I'm afraid it's not possible. There's an ancient issue to use $function in update but you're only way is to read, modify and persist the array.
On the other hand if you an array of embedded documents then you can use the $ positional operator.
The $ can be used to $set a value in you simple array but to $push.
> db.test.find()
{ "_id" : ObjectId("56d9570d09e01f20e254d0f3"), "a" : [ 1, 2, 3 ] }
> db.test.update({"_id": ObjectId("56d9570d09e01f20e254d0f3"), "a": 2},
{$push: {a: {$each: [2], $position: "$"}}})
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 2,
"errmsg" : "The value for $position must be a positive numeric value not a String"
}
})
There is no function to get position and update at one go,
so in you problem you need to get position before update and pass it to query - so that means we are operating on 1:1 bassis.
Other approach is to retrieve data - perform in-place update and push full array set back to mongo

How to query parent based on subdocument's _id?

consider the following records:
user record
{
"_id" : ObjectId("5234ccb7687ea597eabee677"),
"class" : [
{ "_id" : ObjectId("5234ccb7687ea597eabee671", "num" : 10, "color" : "blue" },
{ "_id" : ObjectId("5234ccb7687ea597eabee672", "num" : 100, "color" : "blue" }
]
}
this user has two class sub records, now I need a query that finds all users that have class property where "class._id" has a value of at least one users "class._id"
here is a more detail example:
suppose there is four user:
A:{_id:432645624232345,class:[{_id:123,name:'foo'}]}
B:{_id:432645624232555,class:[{_id:555,name:'foo'},{_id:123,name:'foo'}]}
C:{_id:432645344232345,class:[{_id:555,name:'foo'},{_id:111,name:'www'}]}
D:{_id:432644444232345,class:[{_id:222,name:'sss'},{_id:555,name:'www'},{_id:123,name:'foo'}]}
now if B login , I need to query all the user whose class subdocument contains at least one document which's _id==555 or _id==123 (555 and 123 come from B user), in this case the query result should be:
A:{_id:432645624232345,class:[{_id:123,name:'foo'}]} // match _id=123
B:{_id:432645624232555,class:[{_id:555,name:'foo'},{_id:123,name:'foo'}]} //match _id=123 and _id=555
C:{_id:432645344232345,class:[{_id:555,name:'foo'},{_id:111,name:'www'}]} //match _id=555
D:{_id:432644444232345,class:[{_id:222,name:'sss'},{_id:555,name:'www'},{_id:123,name:'foo'}]} ///match _id=123 and _id=555
which is all the user.
so far i get this:
{"class._id" : { $in : ["5234ccb7687ea597eabee671", "5234ccb7687ea597eabee672"] } }
but when different user login the class._id query condition is different. So is there any operator to do this
{"class._id" : { $in : req.user.class } }
hope I made myself clear.
In order to achieve what you want, first you must isolate the class _ids in an array, and then use it in the query argument.
var classIds = [];
var i = 0;
while (i < req.user.class.length) {
classIds.push(req.user.class[i]._id);
i++;
}
After that you can use classIds array in the query:
{"class._id" : { $in : classIds } }
The following query condition would give you all the users that have at least one class with id equal to any of the elements in the given array:
{"class._id" : { $in : ["5234ccb7687ea597eabee671", "5234ccb7687ea597eabee672"] } }
In the array for the $in clause you may provide any id's you needed , comma separated.
In addition, if you needed such, the below query condition should check for existence of nested document within "class" property that has a property "_id" :
{ "class._id" : { $exists : true } }
Both conditions should work no matter if "class._id" is a single-valued property or an array (mongo supports that).

Resources