MongoDB: How to update a nested array value after checking if greater than zero? - mongodb

I'm able to decrease a value in a nested array, but I want to check that the value is greater than zero (so never go to the negative numbers). For example, I have this:
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839af"),
"slots" : [
{
"id" : "V2qlAEk7Wp0tWwlyWSfX7KRZ",
"number" : 5.0
},
.......
.......
{
"id" : "VfB4f8G1KcgRA8qx0aby5nI0",
"number" : 0.0
}]
}
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839ag"),
"slots" : [
{
"id" : "V2qlAEEESbrEB4bwberbResbd",
"number" : 10.0
},
.......
.......
{
"id" : "DFwseEb5enRbfsbre54rtFfds",
"number" : 1.0
}]
}
If I want to decrease the number value of the first document [id=5a3b0bd69c0000c2a1d839af] of the slots.id = V2qlAEk7Wp0tWwlyWSfX7KRZ, I use:
db.getCollection('schedulers').update(
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839af"),
"slots.id" :"V2qlAEk7Wp0tWwlyWSfX7KRZ"
},
{
"$inc":{"slots.$.number":-1}
})
But I don't know how to check if the number is greater than 0 before decrease the value. I tried to use The filtered positional operator $[<identifier>], but I have an error:
db.getCollection('schedulers').update(
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839af"),
"slots.id" :"V2qlAEk7Wp0tWwlyWSfX7KRZ"
},
{
"$inc":{"slots.$[elm].number":-1}
},
{
arrayFilters: [ { "elm.number": {"$gt" : 0}} ]
})
The error is:
cannot use the part (slots of slots.$[elm].number) to traverse the element....
Also, apply the condition in the first part of the query doesn't fix the problem, cause mongo traverses all the array, so also if the number is zero it simply move to another element in the array until it finds number > 0 and when it finds it, it starts to decrease the number value of another element completely ignoring the two id conditions [because now the $ then point to another element of the array]:
db.getCollection('schedulers').update(
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839af"),
"slots.id" :"V2qlAEk7Wp0tWwlyWSfX7KRZ",
"slots.number":{"$gt" : 0}
},
{
"$inc":{"slots.$.number":-1}
})
I think the correct way to obtain what I want is to use the arrayFilters but I don't understand where is the problem.

Fixed using:
db.getCollection('schedulers').update(
{
"_id" : ObjectId("5a3b0bd69c0000c2a1d839af"),
"slots" :
{
"$elemMatch":
{
"id":"V2qlAEk7Wp0tWwlyWSfX7KRZ",
"number":
{
"$gt" : 0
}
}
}
},
{
"$inc":
{
"slots.$.number":-1
}
})
Thanks

Related

Insert new fields to document at given array index in MongoDB

I have the following document structure in a MongoDB collection :
{
"A" : [ {
"B" : [ { ... } ]
} ]
}
I'd like to update this to :
{
"A" : [ {
"B" : [ { ... } ],
"x" : [],
"y" : { ... }
} ]
}
In other words, I want the "x" and "y" fields to be added to the first element of the "A" array without loosing "B".
Ok as there is only one object in A array you could simply do as below :
Sample Collection Data :
{
"_id" : ObjectId("5e7c3cadc16b5679b4aeec26"),
A:[
{
B: [{ abc: 1 }]
}
]
}
Query :
/** Insert new fields into 'A' array's first object by index 0 */
db.collection.updateOne(
{ "_id" : ObjectId("5e7c3f77c16b5679b4af4caf") },
{ $set: { "A.0.x": [] , "A.0.y" : {abcInY :1 }} }
)
Output :
{
"_id" : ObjectId("5e7c3cadc16b5679b4aeec26"),
"A" : [
{
"B" : [
{
"abc" : 1
}
],
"x" : [],
"y" : {
"abcInY" : 1.0
}
}
]
}
Or Using positional operator $ :
db.collection.updateOne(
{ _id: ObjectId("5e7c3cadc16b5679b4aeec26") , 'A': {$exists : true}},
{ $set: { "A.$.x": [] , "A.$.y" : {abcInY :1 }} }
)
Note : Result will be the same, but functionally when positional operator is used fields x & y are inserted to first object of A array only when A field exists in that documents, if not this positional query would not insert anything (Optionally you can check A is an array condition as well if needed). But when you do updates using index 0 as like in first query if A doesn't exist in document then update would create an A field which is an object & insert fields inside it (Which might cause data inconsistency across docs with two types of A field) - Check below result of 1st query when A doesn't exists.
{
"_id" : ObjectId("5e7c3f77c16b5679b4af4caf"),
"noA" : 1,
"A" : {
"0" : {
"x" : [],
"y" : {
"abcInY" : 1.0
}
}
}
}
However, I think I was able to get anothe#whoami Thanks for the suggestion, I think your first solution should work. However, I think I was able to get another solution to this though I'm not sure if its better or worse (performance wise?) than what you have here. My solution is:
db.coll.update( { "_id" : ObjectId("5e7c4eb3a74cce7fd94a3fe7") }, [ { "$addFields" : { "A" : { "x" : [ 1, 2, 3 ], "y" : { "abc" } } } } ] )
The issue with this is that if "A" has more than one array entry then this will update all elements under "A" which is not something I want. Just out of curiosity is there a way of limiting this solution to only the first entry in "A"?

How to remove an element in double nested array in MongoDB

I'm working on mongo, in this time in need to remove a specific element from array nested in another one, the structure is like the following:
{
"_id" : ObjectId("5e616314946b6d3ac4ed8252"),
"uid" : "5db069478556622b4a0adca5",
"areas" : [
{
"name" : "mexico",
"elements" : [
"23452345",
"24454675"
],
},
{
"name" : "usa",
"elemets" : [
"123123123",
"1234334"
],
}
]
}
So I need a query that lets me remove any element in "elements" property.
I tried with this query but I couldn't reach it.
db.areas.update(
{ },
{ $pull:
{ areas: {
$elemMatch: {
elements: { $eq : "123123123" }
}
}
}
},
{ multi:false }
);
This just said
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 0 })
Here is the update for removing the specific array element in a sub-array (elements).
Note that to remove (pull) the specified element (elements value "24454675"), you need to specify the outer-array's element matching condition (note that there are two elements in the outer-array field areas).
db.areas.update(
{ "areas.name": "mexico" },
{ $pull: { "areas.$.elements": "24454675" } }
)
NOTE: If you don't specify the { "areas.name": "mexico" } condition, the whole sub-document of the areas array will be removed.

Retrieving value of an emedded object in mongo

Followup Question
Thanks #4J41 for your spot on resolution. Along the same lines, I'd also like to validate one other thing.
I have a mongo document that contains an array of Strings, and I need to convert this particular array of strings into an array of object containing a key-value pair. Below is my curent appraoch to it.
Mongo Record:
Same mongo record in my initial question below.
Current Query:
templateAttributes.find({platform:"V1"}).map(function(c){
//instantiate a new array
var optionsArray = [];
for (var i=0;i< c['available']['Community']['attributes']['type']['values'].length; i++){
optionsArray[i] = {}; // creates a new object
optionsArray[i].label = c['available']['Community']['attributes']['type']['values'][i];
optionsArray[i].value = c['available']['Community']['attributes']['type']['values'][i];
}
return optionsArray;
})[0];
Result:
[{label:"well-known", value:"well-known"},
{label:"simple", value:"simple"},
{label:"complex", value:"complex"}]
Is my approach efficient enough, or is there a way to optimize the above query to get the same desired result?
Initial Question
I have a mongo document like below:
{
"_id" : ObjectId("57e3720836e36f63695a2ef2"),
"platform" : "A1",
"available" : {
"Community" : {
"attributes" : {
"type" : {
"values" : [
"well-known",
"simple",
"complex"
],
"defaultValue" : "well-known"
},
[......]
}
I'm trying to query the DB and retrieve only the value of defaultValue field.
I tried:
db.templateAttributes.find(
{ platform: "A1" },
{ "available.Community.attributes.type.defaultValue": 1 }
)
as well as
db.templateAttributes.findOne(
{ platform: "A1" },
{ "available.Community.attributes.type.defaultValue": 1 }
)
But they both seem to retrieve the entire object hirarchy like below:
{
"_id" : ObjectId("57e3720836e36f63695a2ef2"),
"available" : {
"Community" : {
"attributes" : {
"type" : {
"defaultValue" : "well-known"
}
}
}
}
}
The only way I could get it to work was with find and map function, but it seems to be convoluted a bit.
Does anyone have a simpler way to get this result?
db.templateAttributes.find(
{ platform: "A1" },
{ "available.Community.attributes.type.defaultValue": 1 }
).map(function(c){
return c['available']['Community']['attributes']['type']['defaultValue']
})[0]
Output
well-known
You could try the following.
Using find:
db.templateAttributes.find({ platform: "A1" }, { "available.Community.attributes.type.defaultValue": 1 }).toArray()[0]['available']['Community']['attributes']['type']['defaultValue']
Using findOne:
db.templateAttributes.findOne({ platform: "A1" }, { "available.Community.attributes.type.defaultValue": 1 })['available']['Community']['attributes']['type']['defaultValue']
Using aggregation:
db.templateAttributes.aggregate([
{"$match":{platform:"A1"}},
{"$project": {_id:0, default:"$available.Community.attributes.type.defaultValue"}}
]).toArray()[0].default
Output:
well-known
Edit: Answering the updated question: Please use aggregation here.
db.templateAttributes.aggregate([
{"$match":{platform:"A1"}}, {"$unwind": "$available.Community.attributes.type.values"},
{$group: {"_id": null, "val":{"$push":{label:"$available.Community.attributes.type.values",
value:"$available.Community.attributes.type.values"}}}}
]).toArray()[0].val
Output:
[
{
"label" : "well-known",
"value" : "well-known"
},
{
"label" : "simple",
"value" : "simple"
},
{
"label" : "complex",
"value" : "complex"
}
]

mongodb findAndModify update element in array

There is an bson document:
{
"_id" : ObjectId("5718441f5116a60b08000b8c"),
"mails" : [
{
"id" : 2,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
},
{
"id" : 1,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
}
]
}
I need to return and clear the array "a" which belong to "mails.id == x" for given document. So I use findAndModify like:
db.mail.findAndModify({query: {"_id":ObjectId("5718441f5116a60b08000b8c")}, update: {$set:{"mails.$.a":[]}}, new: false, fields:{"mails":{$elemMatch:{"id":1}}}})
However this don't work. The problem is the $set should apply on one document in array rather than the whole document. So I need a projection to project it out.
If I left update to blank, it will return the desired part:
{
"_id" : ObjectId("5718441f5116a60b08000b8c"),
"mails" : [
{
"id" : 1,
"a" : [
{
"a" : 1
},
{
"a" : 2
}
]
}
]
}
But I don't know how to clear the array 'a' in 'mails'
You have to specify array element match in the query:
db.mail.findAndModify({query: {"_id":ObjectId("5718441f5116a60b08000b8c"), "mails":{$elemMatch:{"id":1}}}, update: {$set:{"mails.$.a":[]}}, new: false, fields:{"mails":{$elemMatch:{"id":1}}}})
mails.$ in you update matches the first matched element in the doc, so you have to match it in the query. Also, this query will update the doc, but it will return the old version, since you use new: false, if you want to get the updated version set it to true.

MongoDB MapReduce, second argument of reduce function is multidimensional array

I tried to use mapReduce for my collection. Just for debug I returned vals value passed as second argument do reduce function, like this:
db.runCommand({
"mapreduce":"MyCollection",
"map":function() {
emit( {
country_code:this.cc,
partner:this.di,
registeredPeriod:Math.floor((this.ca - 1399240800)/604800)
},
{
count:Math.ceil((this.lla - this.ca)/86400)
});
},
"reduce":function(k, vals) {
return {
'count':vals
};
},
"query":{
"ca":{
"$gte":1399240800
},
"di":405,
"cc":"1"
},
"out":{
"inline":true
}
});
And I got result like this:
{
"results" : [
{
"_id" : {
"country_code" : "1",
"distribution" : 405,
"installationPeriod" : 0
},
"value" : {
"count" : [
{
"count" : 37
},
{
"count" : 38
}
]
}
},
{
"_id" : {
"country_code" : "1",
"distribution" : 405,
"installationPeriod" : 1
},
"value" : {
"count" : 36
}
},
{
"_id" : {
"country_code" : "1",
"distribution" : 405,
"installationPeriod" : 4
},
"value" : {
"count" : [
{
"count" : [
{
"count" : 16
},
{
"count" : 16
}
]
},
{
"count" : 15
}
]
}
}
],
"timeMillis" : 38,
"counts" : {
"input" : 130,
"emit" : 130,
"reduce" : 5,
"output" : 6
},
"ok" : 1
}
I really don't know why I got multidimensional array as second argument for my reduce function. I mean about this part of result:
{
"_id" : {
"country_code" : "1",
"distribution" : 405,
"installationPeriod" : 4
},
"value" : {
"count" : [
{
"count" : [ // <= Why is this multidimensional?
{
"count" : 16
}
Why is this multidimensional? And why key of embedded array is same like returned from reduce function?
The reason is because this is mapReduce works. From the documentation point:
MongoDB can invoke the reduce function more than once for the same key. In this case, the previous output from the reduce function for that key will become one of the input values to the next reduce function invocation for that key.
And a later point:
the type of the return object must be identical to the type of the value emitted by the map function to ensure that the following operations is true:
So even though you have not "changed the signature" as that documentation points to, you are still only processing n items at once in one reduce pass and then another n items in the next pass. What happens in the eventual processing of this is that the array that was returned in one fragment is combined with the array from another fragment.
So what happened is your reduce returns an array, but it is not "all" of the items you emitted for the key, just some of them. Then another reduce on the same "key" processes more items. Finally those two arrays (or probably more) are again sent to the reduce, in an attempt to actually "reduce" those items as is intended.
That is the general concept, so it is no surprise that when you are just pushing back the array then that is what you get.
Short version, mapReduce processes the ouput "keys" in chunks and not all at once. Better to learn that now before it becomes a problem for you later.