Updating multiple items in nested array when condition matched - mongodb

I started learning MongoDB a few days ago and trying to wrap my head around update operator.
Here's the document structure I have:
db.records.insertMany([
{
name: 'first',
subrecords: [
{ id: 1, isToBeUpdated: 0, value: 1 },
{ id: 2, isToBeUpdated: 0, value: 2 },
{ id: 3, isToBeUpdated: 0, value: 3 },
{ id: 4, isToBeUpdated: 1, value: 4 }
]
},
{
name: 'second',
subrecords: [
{ id: 22, isToBeUpdated: 1, value: 5 },
{ id: 23, isToBeUpdated: 0, value: 6 },
{ id: 24, isToBeUpdated: 1, value: 7 },
{ id: 25, isToBeUpdated: 0, value: 8 }
]
}
])
I'm writing a command that is supposed to update subrecords.
My intent is to use isToBeUpdated flag — if it is set to 1 we update the value field, e.g. multiplying it by 10. We ignore items with isToBeUpdated being 0.
Here's the command I've composed:
db.records.update(
{ 'subrecords.isToBeUpdated': 1 },
{ '$mul': { 'subrecords.$.value': 10 } },
{ multi: true }
)
I expect to get the following items of subrecords array updated like this:
1. { id: 4, isToBeUpdated: 1, value: 40 }
2. { id: 22, isToBeUpdated: 1, value: 50 }
3. { id: 24, isToBeUpdated: 1, value: 70 }
BUT it just updates 2 records, instead of 3. The following record is not updated:
3. { id: 24, isToBeUpdated: 1, value: 70 }
It is the second item with isToBeUpdated being 1 located in the second document's subrecords collection — seems like the query only updates the first matching item of subrecords array in any document. And ignores other records that are subsequent to first.
I tried tweaking my command adding [] to $ (positional operator):
... { '$mul': { 'subrecords.$[].value': 10 } }, ...
But in this case literally every single item in subrecords array of any document gets affected.
Anyone knows how to write a command which loops through all records and updates each item of the nested subrecords array with isToBeUpdated field being set to 1?
Also, am I right in thinking
that { multi: true } implies that ALL record documents in records collection will be affected?
that { 'subrecords.isToBeUpdated': 1 } (query part) literally means that WE ONLY UPDATE subrecords ARRAY WHEN A DOCUMENT HAS AT LEAST ONE ITEM IN subrecords ARRAY WITH isToBeUpdated FIELD BEING SET TO 1?

The workaround turns out to be arrayFilters
https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/#update-all-documents-that-match-arrayfilters-in-an-array
The $[<identifier>] operator facilitates updates to arrays that contain embedded documents. To access the fields in the embedded documents, use the dot notation on the $[<identifier>].
db.records.update(
{},
{ '$mul': { 'subrecords.$[elem].value': 10 } },
{
multi: true,
arrayFilters: [{ 'elem.isToBeUpdated': 1 }]
}
)
Solved!

Related

What is the mongo compound index keyword arrangement rule? Numbers first, strings later?

When I used numbers as keys, the compound index prefixes created were not as expected。
To define a compound index:
admin> use TestDB
TestDB> db.createCollection('coll')
TestDB> db.coll.createIndex({4:1,1:-1},{unique:true})
TestDB> db.coll.getIndexex()
Output
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { '1': -1, '4': 1 }, name: '1_-1_4_1', unique: true }
]
I expected 4 to be the prefix of the index, but it turned out to be 1, why?
It looks like the sorting happens
And when I use strings as keywords, the results are completely inconsistent。
Following:
TestDB> db.createCollection("coll2")
TestDB> db.coll2.createIndex({'s':1, 'a':-1},{unique:true})
Output
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { s: 1, a: -1 }, name: 's_1_a_-1', unique: true }
]
What? It doesn't seem to be sorting.
According to this issue, which references the official specification, this is a result of how Javascript works.
For example, in Node:
$ node
Welcome to Node.js v16.17.0.
Type ".help" for more information.
> var pattern = { 4: 1, 1: -1 }
undefined
> pattern
{ '1': -1, '4': 1 }
You can see similar behavior in this jsfiddle.
If you are attempting to create indexes that have numeric keys, you will need to do so with a different language/driver.

MongoDB: Update outer array and nested arrays in single update

Document structure in cities collection is like this
cities
{
_id: ObjectId("5e78ec62bb5b406776e92fac"),
city_name: "Mumbai",
...
...
subscriptions: [
{
_id: 1,
category: "Print Magazine",
subscribers: 183476
options: [
{
name: "Time",
subscribers: 56445
},
{
name: "The Gentlewoman",
subscribers: 9454
},
{
name: "Gourmand",
subscribers: 15564
}
...
...
]
},
{
_id: 2,
category: "RSS Feed",
subscribers: 2645873
options: [
{
name: "Finance",
subscribers: 168465
},
{
name: "Politics",
subscribers: 56945
},
{
name: "Entrepreneurship",
subscribers: 56945
},
...
...
]
}
]
}
Now when a user subscribes like below
{
cityId: 5e78ec62bb5b406776e92fac
selections: [
{
categoryId: 1,
options : ["Time", "Gourmand"]
},
{
categoryId: 2,
selected: ["Politics", "Entrepreneurship"]
}
]
}
I want to update the following in the cities document
Increment subscribers for "Print Magazine" by 1
Increment subscribers for "Time" by 1
Increment subscribers for "Gourmand" by 1
Increment subscribers for "RSS Feed" by 1
Increment subscribers for "Politics" by 1
Increment subscribers for "Entrepreneurship" by 1
So when an item is subscribed, its subscribers count is incremented by 1. And the category it falls into, its subscriber count is also incremented by 1.
I want to achieve this in a single update query. Any tips how can I do this?
Use case details
Each user's subscription details are stored in user_subscription_details collection(not listed here). subscriptions property in cities holds just the subscription summary for each city.
So I was able to it with the following query
db.cities.updateOne(
{
_id : ObjectId("5e78ec62bb5b406776e92fac")
},
{
$inc: {
"subscriptions.$[category].subscribers" : 1,
"subscriptions.$[category].options.$[option].subscribers" : 1
}
},
{ multi: true,
arrayFilters: [
{ "category._id": {$in: ["1", "2"]} },
{ "option.name": {$in: ["Time", "Gourmand", "Politics", "Entrepreneurship"]} }
]
}
)
Brief Explanation
First the document is matched with _id.
In update block we will declare the fields to be updated
"subscriptions.$[?].subscribers" : 1,
"subscriptions.$[?].options.$[?].subscribers" : 1
I have used ? here to show we don't know yet for which elements in the array we need to do these update. Which we can declare in the next block by filtering the array elements that need to be updated.
In filter block we filter array elements on some condition
{ "category._id": {$in: ["1", "2"]} }
{ "option.name": {$in: ["Time", "Gourmand", "Politics", "Entrepreneurship"]} }
First we filter the elements in the outer array by _id i.e only subscription categories whose _id is either 1 or 2.
Next, we filter the elements in the inner options array on the name field. Elements which will pass both filters will get updated.
Note: category in category._id and option in option.name can be any name. But the same name is to be used for fields path in update block.
For, Spring Boot MongoOperation translation of this query look at this answer

mongodb: add an attribute to a subdocument

Given a collection of Users:
db.users.insertMany(
[
{
_id: 1,
name: "sue",
points: [
{ points: 85, bonus: 20 },
{ points: 85, bonus: 10 }
]
},
{
_id: 2,
name: "bob",
points: [
{ points: 85, bonus: 20 },
{ points: 64, bonus: 12 }
]
}]);
How do I add an attribute bonus_raw in every points, with a copy of the value of bonus value? I tried:
db.getCollection('users').update({ },
{$set:{ 'points.$.bonus_raw' : 'points.$.bonus' }}, false, true)
but I get:
The positional operator did not find the match needed from the query. Unexpanded update: points.$.bonus_raw
Updating multiple items in an array is not possible as of now in MongoDB.
To get this done, you will have to query the document, loop over all of your nested documents, and then save it back to MongoDB.
In your case, this can help:-
db.users.find({points: { $exists: true } }).forEach(function (doc){
doc.points.forEach(function (points) {
points.bonus_raw = points.bonus;
});
db.users.save(doc)
});
Also, take care of race conditions while doing an update in this way. See this

Find And Update in array of subdocuments

I'm I newbie in mongodb world, and I've already spent a lot of time trying to resolve the following problem.
My collection is called 'routes' with the documents:
{
idGateway: 0,
priority: 1,
channels: [
{
id: 'chanel_1',
control: {
timestamp: ISODate("2015-09-19T16:17:12.393Z"),
qty: 3
}
},
{
id: 'chanel_2',
control: {
timestamp: ISODate("2015-09-18T16:17:12.393Z"),
qty: 5
}
}
]
},
{
idGateway: 1,
priority: 2,
channels: [
{
id: 'chanel_3',
control: {
timestamp: ISODate("2015-09-19T16:17:12.393Z"),
qty: 3
}
},
{
id: 'chanel_4',
control: {
timestamp: ISODate("2015-09-18T16:17:12.393Z"),
qty: 5
}
}
]
}
My needs are select a document and at the same time update its timestamp/qty fields (as a FindAndModify). The criteria to selection is:
Get the document with the lowest priority
From the document, select the channels' document with the MAX(timestamp)
Update timestamp and qty
And return only the channels' document (channel_1).
So, once I've selected the channel_1 I have to avoid that other process select it again befor the update. How can I do this ?
Thanks

MongoDB update outter and array-embedded document atomically

I have an object in the cart collection:
{
_id: 1,
purchased: 0,
items: [
{
item_id: 5,
count: 0
},
{
item_id: 6,
count: 0
},
]
}
And I would like to atomically increment both purchased and count of item_id: 6. I believe the correct update command is:
db.carts.update({_id: 1, "items.item_id": 6},
{
$inc: {
purchased: 1
"items.$.count": 1
}
})
This seems to work in the console but I am not sure if this is 100% correct. Can anyone spot any issues with this update command or confirm this is correct for my use case?
For scaling this to my app, suppose _id is an ObjectId and item_id is also ObjectId (ie they are unique).