Insert an array item after a specific value in mongodb - node.js

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

Related

How to update array of object in robomongo [duplicate]

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"

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"
]
}

Query for a list contained in another list in mongodb

I'm fairly new to mongo and while I can manage to do most basic operations with the $in, $or, $all, ect I can't make what I want to work.
I'll basically put a simple form of my problem. Part of my documents are list of number, eg :
{_id:1,list:[1,4,3,2]}
{_id:2,list:[1]}
{_id:3,list:[1,3,4,6]}
I want a query that given a list(lets call it L), would return me every document where their entire list is in L
for example with the given list L = [1,2,3,4,5] I want document with _id 1 and 2 to be returned. 3 musn't be returned since 6 isn't in L.
"$in" doesn't work because it would also return _id 3 and "$all" doesn't work either because it would only return _id 1.
I then thought of "$where" but I can't seem to find how to bound an external variable to the js code. What I call by that is that for example :
var L = [1,2,3,4,5];
db.collections('myCollection').find({$where:function(l){
// return something with the list "l" there
}.bind(null,list)})
I tried to bind list to the function as showed up there but to no avail ...
I'd glady appreciate any hint concerning this issue, thanks.
There's a related question Check if every element in array matches condition with an answer with a nice approach for this scenario. It refers to an array of embedded documents but can be adapted for your scenario like this:
db.list.find({
"list" : { $not : { $elemMatch : { $nin : [1,2,3,4,5] } } },
"list.0" : { $exists: true }
})
ie. the list must not have any element that is not in [1,2,3,4,5] and the list must exist with at least 1 element (assuming that's also a requirement).
You could try using the aggregation framework for this where you can make use of the set operators to achieve this, in particular you would need the $setIsSubset operator which returns true if all elements of the first set appear in the second set, including when the first set equals the second set; i.e. not a strict subset.
For example:
var L = [1,2,3,4,5];
db.collections('myCollection').aggregate([
{
"$project": {
"list": 1,
"isSubsetofL": {
"$setIsSubset": [ "$list", L ]
}
}
},
{
"$match": {
"isSubsetofL": true
}
}
])
Result:
/* 0 */
{
"result" : [
{
"_id" : 1,
"list" : [
1,
4,
3,
2
],
"isSubsetofL" : true
},
{
"_id" : 2,
"list" : [
1
],
"isSubsetofL" : true
}
],
"ok" : 1
}

Complex search and extendable query on two and more collections

I am working on Node.js + mongodb application and my question related to design of my database and queries. Firstly, describe my db structure. I have in my database two collections users and values. Document in users collection looks like this:
{
"_id" : 31450861,
"first_name" : "Jon",
"last_name" : "Doe",
"sex" : 2,
"bdate" : ISODate("1981-08-01T21:00:00Z"),
"city" : {
"id" : 282,
"title" : "Minsk"
},
"country" : {
"id" : 3,
"title" : "Belarussia"
},
"photo_max" : "https://foto.ru/RnhOKp2YJE4.jpg",
"relation" : 4,
"interests" : "science",
"music" : "pop",
"lang" : "RU",
}
This collection filled with users data (language, birthday, etc).
Documents from collection values looks like this:
{
"_id" : ObjectId("548023e5c16f310c23b75075"),
"userId" : "31450861", //reference to _id in users collection
"group1" : {
"value1" : 13,
" value2" : 7,
" value3" : 3,
" value4" : 14,
" value5" : 17
},
"group2" : {
" value1" : 9,
" value2" : 6,
" value3" : 17,
" value4" : 12,
" value5" : 13
}
So I need to make search for users on this database with lots of parameters from this two collection (their values (from collection values), sex, city, language etc.). I didn’t embed document values into users because I do a lot queries separately on them (but may be it’s anyway wrong design I need help on this). In future there will be more collections with similar structure like values (or at least there will be reference to userId), which I’ll have to include in search query, and I’ll need agility to extend my query on more collections.
So I need to run complex query on this collections (I know there is no JOINs in mongodb, so I know I have to query twice or use mapreduce).
So far my thoughts on this issue (aren’t tested in code just thoughts).
I need to write search function which performs 2 queries (and more in future):
Find users with same values and getting their ids.
var values = db.collection('values');
var ids = values.find({ value1: 1, value2: 2, value3: 3 }, {userId: 1 } ) //then transform ids so it became array with userId
Then in this found set find users on some more parameters (sex, birthday, language, etc)
var users = db.collection('users');
users.find({ $and: [{ _id: { $in: ids } }, {sex: 2 }, {lang: “RU” } ] });
My questions are:
1. Is it normal approach or I’ll end up with very slow performance and mess in code when adding new collections and queries?
2. If is it normal, how to easily add one more query to one more collection?
Any help, any thoughts are welcome! Thanks in advance!

Does $elemMatch not work with a new mongodb 2.2?

In a new version on MongoDB we can use an $elemMatch projection operator to limit the response of a query to a single matching element of an array. http://docs.mongodb.org/manual/reference/projection/elemMatch/
But it seems doesn't work yet in mongoose 3 here is the example:
{
_id: ObjectId(5),
items: [1,2,3,45,4,67,9,4]
}
Folder.findOne({_id: Object(5)}, {$elemMatch: {$in: [1,67,9]}})
.exec(function (err, doc) {
});
I'm expected to get the follows doc:
{
_id: ObjectId(5),
items: [1,67,9]
}
But unfortunately what I'm getting is document with all items:
{
_id: ObjectId(5),
items: [1,2,3,45,4,67,9,4]
}
The mongodb docs here are misleading, we'll get them updated.
What its saying is that you can now use $elemMatch in your projection, that is, your field selection:
https://gist.github.com/3640687
See also: https://github.com/learnboost/mongoose/issues/1085
[Edit] pull request for docs sent: https://github.com/mongodb/docs/pull/185
Firstly, you are missing the items field name in front of the $elemMatch operator. Your query should read
Folder.findOne({_id: Object(5)}, {items: {$elemMatch: {$in: [1,67,9]}}})
.exec(function (err, doc) { });
But this would still not return the desired result, because as stated in the documentation:
The $elemMatch projection will only match one array element per source
document.
So you would only get back something like:
{
_id: ObjectId(5),
items: [1]
}
I haven't got mongoose set up to do this with node, but you can also get the result you want using the new aggregation framework in 2.2 - here's an example that gets you the result you wanted. First, my sample doc looks like this:
> db.foo.findOne()
{
"_id" : ObjectId("50472eb566caf6af6108de02"),
"items" : [
1,
2,
3,
45,
4,
67,
9,
4
]
}
To get to what you want I did this:
> db.foo.aggregate(
{$match : {"_id": ObjectId("50472eb566caf6af6108de02")}},
{$unwind : "$items"},
{$match : {"items": {$in : [1, 67, 9]}}},
{$group : {_id : "$_id", items : { $push : "$items"}}},
{$project : {_id : 0, items : 1}}
)
{
"result" : [
{
"_id" : ObjectId("50472eb566caf6af6108de02"),
"items" : [
1,
67,
9
]
}
],
"ok" : 1
}
To explain, in detail I will take it line by line:
{$match : {"_id": ObjectId("50472eb566caf6af6108de02")}}
This is fairly obvious - it is basically the equivalent to the find criteria on a regular query, the results are passed to the next step in the pipeline to be processed. This is the piece that can use indexes etc.
{$unwind : "$items"}
This will explode the array, creating a stream of documents, one for each element of the array.
{$match : {"items": {$in : [1, 67, 9]}}}
This second match will return only the documents in the list, basically reducing the stream of docs to a result set of three.
{$group : {_id : "$_id", items : { $push : "$items"}}}
We want our output to be an array, so we have to undo the unwind above now that we have selected the items we want, using the _id as the key to group. Note: this will have repeating values if there is more than one match, if you wanted a unique list you would use $addToSet instead of $push
{$project : {_id : 1, items : 1}}
Then finally, this projection is not really needed, but I included it to illustrate the functionality - you could choose to not return the _id if you wished etc.
Neither $elemMatch nor MongoDB in general will filter data from an array. $elemMatch can be used to match a document but it won't affect the data to be returned. You can only include/exclude fields from a documented by using the filter parameter (second parameter of a find() findOne() call) but you can not filter the result based on some query input.

Resources