MongoDB: update specific elements in array - mongodb

I have a documents with the following structure:
{
_id: ObjectId(),
"subjects": [
{
"name": "math",
"first_try": {
"passed": true
},
"second_try": {
"passed": false
},
"third_try": {
"passed": false
},
"fourth_try": {
"passed": false
}
}
]
}
There are a couple of such subjects there.
Please don't suggest to change data structure, etc. - it's a fake data structure created just for this question (can't share original names, but structure is the same).
For each of these subjects I have always these 4 keys: "first_try", "second_try", ..., "fourth_try". Some of them are "passed", some - not.
For each subject I want to set first_try.passed: true if there weren't other passed tries.If for example third_try.passed:true I shouldn't update first_try.
I was trying to proceed with some aggregate conditions including elemMatch to find items for update, but it looks awful and didn't work as I expect.
Is it possible to handle this case with single updateMany query?

You can use arrayFilters:
collection.updateMany({},
{$set: {"subjects.$[element].first_try.passed": true}},
{arrayFilters: [{
"element.first_try.passed":false,
"element.second_try.passed":false,
"element.third_try.passed":false,
"element.fourth_try.passed":false}]})

Related

Update a nested field with an unknown index and without affecting other entries

I have a collection with a layout that looks something like this:
student1 = {
"First_Name": "John",
"Last_Name": "Doe",
"Courses": [
{
"Course_Id": 123,
"Course_Name": "Computer Science",
"Has_Chosen_Modules": false
},
{
"Course_Id": 284,
"Course_Name": "Mathematics",
"Has_Chosen_Modules": false
}
]
};
I also have the following update query:
db.Collection_Student.update(
{
$and: [
{First_Name: "John"},
{Last_Name: "Doe"}
]
},
{
$set : { "Courses.0.Has_Chosen_Modules" : true }
}
);
This code will currently update the Computer Science Has_Chosen_Modules value to true since the index is hardcoded. However, what if I wanted to update the value of Has_Chosen_Modules via the Course_Id instead (as the course might not necessarily be at the same index every time)? How would I achieve this without it affecting the other courses that a given student is taking?
You can select any item in the sub array of your document by targeting any property in the sub array of your document by using dot .
You can easily achieve this by the following query.
db.Collection_Student.update(
{
First_Name: "John",
Last_Name: "Doe",
'Courses.Course_Id': 123
},
{
$set : { "Courses.$.Has_Chosen_Modules" : true }
}
);
Conditions in search filter are by default treated as $and operator, so you don't need to specifically write $and for this simple query.

Match multiple criteria inside an array [duplicate]

This question already has an answer here:
MongoDB : find value in Array with multiple criteria
(1 answer)
Closed 3 years ago.
I have the following:
offers: [
{user: 'jon', price: 200, selected: false},
{user: 'ted', price: 100, selected: true}
]
I am trying to do a match that will get all offer objects where user is ted and selected is true.
I tried the following:
$match: {
"offers.user": "ted"
"offers.selected": true
}
But this will give me the document if there is a ted and a selected true inside the array and not necessary that combination inside the same object.
You need to use $elemMatch query operator to match multiple criteria inside an array
{ "$match": {
"offers": {
"$elemMatch": {
"user": "ted"
"selected": true
}
}
}}
You want mongoose elemMatch function for your case.
query.elemMatch('comment', { author: 'autobot', votes: {$gte: 5}})
See the docs here:

Updating multiple subdocument arrays in MongoDB

I have a collection full of products each of which has a subdocument array of up to 100 variants (SKUs) of that product:
e.g.
{
'_id': 12345678,
'handle': 'my-product-handle',
'updated': false
'variants': [
{
'_id': 123412341234,
'sku': 'abc123',
'inventory': 1
},
{
'_id': 123412341235,
'sku': 'abc124',
'inventory': 2
},
...
]
}
My goal is to be able to update the inventory quantity of all instances of a SKU number. It is important to note that in the system I'm working with, SKUs are not unique. Therefore, if a SKU shows up multiple times in a single product or across multiple products, they all need to be updated to the new inventory quantity.
Furthermore, I need the "updated" field to be changed to "true" *only if the inventory quantity for that SKU has changed"
As an example, if I want to update all instances of SKU "abc123" to have 25 inventory, the example of above would return this:
{
'_id': 12345678,
'handle': 'my-product-handle',
'updated': true
'variants': [
{
'_id': 123412341234,
'sku': 'abc123',
'inventory': 25
},
{
'_id': 123412341235,
'sku': 'abc124',
'inventory': 2
},
...
]
}
Thoughts?
MongoDB 3.6 has introduced the filtered positional operator $[<identifier>] which can be used to update multiple elements of an array which match an array filter condition. You can read more about this operator here: https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/
For example, to update all elements of the variants array where sku is "abc123" across every document in the collection:
db.collection.update({}, { $set: { "variants.$[el].inventory": 25 }}, { multi: true, arrayFilters: [{ "el.sku": "abc123"}] })
Unfortunately I'm not aware of any way in a single query to update a document's field based on whether another field in the document was updated. This is something you would have to implement with some client-side logic and a second query.
EDIT (thanks to Asya's comment):
You can do this in a single query by only matching documents which will be modified. So if nMatched and nModified are necessarily equal, you can just set updated to true. For example, I think this would solve the problem in a single query:
db.collection.update({ variants: { $elemMatch: { inventory: { $ne: 25 }, sku: "abc123" } } }, { $set: { "variants.$[el].inventory": 25, updated: true }}, { multi: true, arrayFilters: [{ "el.sku": "abc123"}] })
First you match documents where the variants array contains documents where the sku is "abc123" and the inventory does not equal the number you are setting it to. Then you go ahead and set the inventory on all matching subdocuments and set updated to true.

Update Array Children Sorted Order

I have a collection containing objects with the following structure
{
"dep_id": "some_id",
"departament": "dep name",
"employees": [{
"name": "emp1",
"age": 31
},{
"name": "emp2",
"age": 35
}]
}
I would like to sort and save the array of employees for the object with id "some_id", by employees.age, descending. The best outcome would be to do this atomically using mongodb's query language. Is this possible?
If not, how can I rearrange the subdocuments without affecting the parent's other data or the data of the subdocuments? In case I have to download the data from the database and save back the sorted array of children, what would happen if something else performs an update to one of the children or children are added or removed in the meantime?
In the end, the data should be persisted to the database like this:
{
"dep_id": "some_id",
"departament": "dep name",
"employees": [{
"name": "emp2",
"age": 35
},{
"name": "emp1",
"age": 31
}]
}
The best way to do this is to actually apply the $sort modifier as you add items to the array. As you say in your comment "My actual objects have a "rank" and 'created_at'", which means that you really should have asked that in your question instead of writing a "contrived" case ( don't know why people do that ).
So for "sorting" by multiple properties, the following reference would adjust like this:
db.collection.update(
{ },
{ "$push": { "employees": { "$each": [], "$sort": { "rank": -1, "created_at": -1 } } } },
{ "multi": true }
)
But to update all the data you presently have "as is shown in the question", then you would sort on "age" with:
db.collection.update(
{ },
{ "$push": { "employees": { "$each": [], "$sort": { "age": -1 } } } },
{ "multi": true }
)
Which oddly uses $push to actually "modify" an array? Yes it's true, since the $each modifier says we are not actually adding anything new yet the $sort modifier is actually going to apply to the array in place and "re-order" it.
Of course this would then explain how "new" updates to the array should be written in order to apply that $sort and ensure that the "largest age" is always "first" in the array:
db.collection.update(
{ "dep_id": "some_id" },
{ "$push": {
"employees": {
"$each": [{ "name": "emp": 3, "age": 32 }],
"$sort": { "age": -1 }
}
}}
)
So what happens here is as you add the new entry to the array on update, the $sort modifier is applied and re-positions the new element between the two existing ones since that is where it would sort to.
This is a common pattern with MongoDB and is typically used in combination with the $slice modifier in order to keep arrays at a "maximum" length as new items are added, yet retain "ordered" results. And quite often "ranking" is the exact usage.
So overall, you can actually "update" your existing data and re-order it with "one simple atomic statement". No looping or collection renaming required. Furthermore, you now have a simple atomic method to "update" the data and maintain that order as you add new array items, or remove them.
In order to get what you want you can use the following query:
db.collection.aggregate({
$unwind: "$employees" // flatten employees array
}, {
$sort: {
"employees.name": -1 // sort all documents by employee name (descending)
}
}, {
$group: { // restore the previous structure
_id: "$_id",
"dep_id": {
$first: "$dep_id"
},
"departament": {
$first: "$departament"
},
"employees": {
$push: "$employees"
},
}
}, {
$out: "output" // write everything out to a separate collection
})
After this step you would want to drop your source table and rename the "output" collection to match your source table name.
This solution will, however, not deal with the concurrency issue. So you should remove write access from the collection first so nobody modifies it during the process and then restore it once you're done with the migration.
You could alternatively query all data first, then sort the employees array on the client side and then use either single update queries or - faster but more complicated - a bulk write operation with all the individual update calls in order to update the existing documents. Here, you could use the entire document that you've initially read as a filter for the update operation. So if an individual update does not modify any document you'd know straight away, that some other change must have modified the document you read before. Those cases you'd need to retry later (or straight away until the update does actually modify a document).

mongodb $addToSet to a non-array field when update on upsert

My recent project encountered the same problem as this one: the question
db.test.update(
{name:"abc123", "config.a":1 },
{$addToSet:{ config:{a:1,b:2} } },
true
)
Will produce such error:
Cannot apply $addToSet to a non-array field
But after changed to:
db.test.update(
{name:"abc123", "config.a":{$in:[1]} },
{$addToSet:{ config:{a:1,b:2} } },
true
)
It works fine.
Also referenced this link: Answer
Can Any one explain what's going on? "config.a":1 will turn config to be an object? Where "config.a":{$in:[1]} won't?
What you are trying to do here is add a new item to an array only where the item does not exist and also create a new document where it does not exist. You choose $addToSet because you want the items to be unique, but in fact you really want them to be unique by "a" only.
So $addToset will not do that, and you rather need to "test" the element being present. But the real problem here is that it is not possible to both do that and "upsert" at the same time. The logic cannot work as a new document will be created whenever the array element was not found, rather than append to the array element like you want.
The current operation errors by design as $addToSet cannot be used to "create" an array, but only to "add" members to an existing array. But as stated already, you have other problems with achieving the logic.
What you need here is a sequence of update operations that each "try" to perform their expected action. This can only be done with multiple statements:
// attempt "upsert" where document does not exist
// do not alter the document if this is an update
db.test.update(
{ "name": "abc" },
{ "$setOnInsert": { "config": [{ "a": 1, "b": 2 }] }},
{ "upsert": true }
)
// $push the element where "a": 1 does not exist
db.test.update(
{ "name": "abc", "config.a": { "$ne": 1 } },
{ "$push": { "config": { "a": 1, "b": 2 } }}
)
// $set the element where "a": 1 does exist
db.test.update(
{ "name": "abc", "config.a": 1 },
{ "$set": { "config.$.b": 2 } }
)
On a first iteration the first statement will "upsert" the document and create the array with items. The second statement will not match the document because the "a" element has the value that was specified. The third statement will match the document but it will not alter it in a write operation because the values have not changed.
If you now change the input to "b": 3 you get different responses but the desired result:
db.test.update(
{ "name": "abc" },
{ "$setOnInsert": { "config": [{ "a": 1, "b": 3 }] }},
{ "upsert": true }
)
db.test.update(
{ "name": "abc", "config.a": { "$ne": 1 } },
{ "$push": { "config": { "a": 1, "b": 3 } }}
)
db.test.update(
{ "name": "abc", "config.a": 1 },
{ "$set": { "config.$.b": 3 } }
)
So now the first statement matches a document with "name": "abc" but does not do anything since the only valid operations are on "insert". The second statement does not match because "a" matches the condition. The third statment matches the value of "a" and changes "b" in the matched element to the desired value.
Subsequently changing "a" to another value that does not exist in the array allows both 1 and 3 to do nothing but the second statement adds another member to the array keeping the content unique by their "a" keys.
Also submitting a statement with no changes from existing data will of course result in a response that says nothing is changed on all accounts.
That's how you do your operations. You can do this with "ordered" Bulk operations so that there is only a single request and response from the server with the valid response to modified or created.
As explained in this issue on the MongoDB JIRA (https://jira.mongodb.org/browse/SERVER-3946), this can be solved in a single query:
The following update fails because we use $addToSet on a field which we have also included in the first argument (the field which accepts the fields and values to query for). As far as I understand it, you can't use upsert: true in this scenario where we $addToSet to the same field we query with.
db.foo.update({x : "a"}, {$addToSet: {x: "b"}} , {upsert: true}); // FAILS
The solution is to use $elemMatch: {$eq: field: value}
db.foo.update({x: {$elemMatch: {$eq: "a"}}}, {$addToSet: {x: "b"}}, {upsert: true});