MongoDB update :- update multiple objects nested inside array (in multiple documents) - mongodb

My collection is as follows :
{
"_id" : "",
"team" : "avenger",
"persons" : [
{
"name" : "ABC",
"class" : "10",
"is_new" : true
},
{
"name" : "JHK",
"class" : "12",
"is_new" : true
},
{
"name" : "BNH",
"class" : "10",
"is_new" : true
}
]
},
{
"_id" : "",
"team" : "adrenaline",
"persons" : [
{
"name" : "HTU",
"class" : "11",
"is_new" : true
},
{
"name" : "NVG",
"class" : "10",
"is_new" : true
},
{
"name" : "SED",
"class" : "8",
"is_new" : true
}
]
}
My Goal is to find for all such documents which contain such nested objects in "persons" where "class" is "10" and update "is_new" field to "false"
I am using mongoose update with "multi" set to true
Query :
{
persons: { $elemMatch: { class: "10" } }
}
Options:
{
multi : true
}
Update :
First one I have tried is :
{
$set : {
"persons.$.is_new" : false
}
}
Second one is :
{
$set : {
"persons.$[].is_new" : false
}
}
The issue is : using $ in update operation updates only first match in the "persons" array for all matched documents.
If I use $[], it updates all the objects of "persons" array in the matched documents.
Workaround can be to use a loop for update operation but i believe that should not be the ideal case.
I see nothing in the docs, though have found people reporting this update operation issue.
Is this can't be done using a single query ?? Is looping through documents my only option ?

You need to set the filtered positional operator $[<identifier>] identifier
Model.update(
{ "persons": { "$elemMatch": { "class": "10" }}},
{ "$set" : { "persons.$[e].is_new" : false }},
{ "arrayFilters": [{ "e.class": "10" }], "multi" : true }
)

Related

Mongodb $nin not working with nested array

I can't make $nin work with nested arrays, can you guys spot any issue with this query?
I'm basically trying to update the status of every document under items to "closed" in case their hash field is not in a list of hashes provided.
db.getCollection('projects').update({
name: 'test',
'issues.hash': { $nin: [
'8ff28fcc9cbf10c9b690bb331e5609efbd3c526be4f536ebca02cc51bd63eac7',
'd5368ad5658ec11103796255d127d26da7f3324cdedbd124bdd5db50812d588e',
'37298229097785ebc9d419cc1a3f13e0d090a15ceb9a8e6bea3505366902556d',
'fad290f2ddd0e097e4098c3b2c3d65611406cf208a3f86924d45c7736393b44b'
]}
},
{
$set: { "issues.$.status": "closed" }
}
)
This is the data:
{
"_id" : ObjectId("611bb4d2ee06769a5f6d906d"),
"name" : "test",
"issues" : [
{
"_id" : ObjectId("611bb4d20b2fb200167aa588"),
"status" : "open",
"hash" : "8ff28fcc9cbf10c9b690bb331e5609efbd3c526be4f536ebca02cc51bd63eac7"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa589"),
"status" : "open",
"hash" : "3b83e469049e46b16d3471a188d3f5e3ddbf6b296995a71765bbf17b7289e6ea"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58a"),
"status" : "open",
"hash" : "bef5f50628b669b9930b89cdc040361b9c8cc2b4aab3c2059c171786d38d507e"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58b"),
"status" : "open",
"hash" : "1b4a91eb5de97d6ad7493b6e1ffa48a2a648084b4af7b37916c723533a07c37c"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58c"),
"status" : "open",
"hash" : "bb64ba7b2612856dcd95c3ac2fad3f7368e5d463168545b12f4c869af56b55b7"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58d"),
"status" : "open",
"hash" : "1d5fc04739b10414dea8d327998df4f200f47ce57da243bd578d4ae102f2d670"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58e"),
"status" : "open",
"hash" : "d5368ad5658ec11103796255d127d26da7f3324cdedbd124bdd5db50812d588e"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa58f"),
"status" : "open",
"hash" : "37298229097785ebc9d419cc1a3f13e0d090a15ceb9a8e6bea3505366902556d"
},
{
"_id" : ObjectId("611bb4d20b2fb200167aa590"),
"status" : "open",
"hash" : "fad290f2ddd0e097e4098c3b2c3d65611406cf208a3f86924d45c7736393b44b"
}
]
}
And the is my result:
Updated 0 record(s) in 12ms
Thank you!
You have to use arrayFilters in this way:
db.collection.update({
"name": "test"
},
{
"$set": {
"issues.$[element].status": "closed"
}
},
{
"arrayFilters": [
{
"element.hash": {
"$nin": [
"8ff28fcc9cbf10c9b690bb331e5609efbd3c526be4f536ebca02cc51bd63eac7",
"d5368ad5658ec11103796255d127d26da7f3324cdedbd124bdd5db50812d588e",
"37298229097785ebc9d419cc1a3f13e0d090a15ceb9a8e6bea3505366902556d",
"fad290f2ddd0e097e4098c3b2c3d65611406cf208a3f86924d45c7736393b44b"
]
}
}
]
})
Example here.
Note that update query has the format: update(query, update, options) (Check the docs).
So with your find query mongo doesn't find anything. Check this example.
This is why you are telling mongo: "Give me a DOCUMENT where name is test and issues array NOT contains a field called hash with these values".
So, as mongo search by the whole document, there is no any document where hash value is not on the $nin array.
As another example to exaplain better: Check this example where hash value is 1. In this case, find query works because it matches two conditions:
There is a field name with value test
There is not any field hash into issues with values into $nin array.
You can use arrayFilters, like this:
db.collection.update({
"name": "test"
},
{
"$set": {
"issues.$[elem].status": "closed"
}
},
{
"multi": true,
"arrayFilters": [
{
"elem.hash": {
"$nin": [
"8ff28fcc9cbf10c9b690bb331e5609efbd3c526be4f536ebca02cc51bd63eac7",
"d5368ad5658ec11103796255d127d26da7f3324cdedbd124bdd5db50812d588e",
"37298229097785ebc9d419cc1a3f13e0d090a15ceb9a8e6bea3505366902556d",
"fad290f2ddd0e097e4098c3b2c3d65611406cf208a3f86924d45c7736393b44b"
]
}
}
]
})
Here is the working example: https://mongoplayground.net/p/8wZkmlBgKiq

build dynamic MongoDB query to update nested value in dict

Considering an object as such:
{
"_id" : ObjectId("5b4dbf3541e5ae6de394cc99"),
"active" : true,
"email" : "admin#something.eu",
"password" : "$2b$12$5qqD3ZulKI7S6j.Wx513POpCNWRMppE.vY4.3EIZedm109VUPqXoi",
"badges" : {
"cnc" : {
"lvl" : "0"
},
"laser" : {
"lvl" : "0"
},
"impression3d" : {
"lvl" : "0"
},
"maker" : {
"lvl" : "0"
},
"electronique" : {
"lvl" : "0"
},
"badge" : {
"lvl" : 1
}
},
"roles" : [
ObjectId("5b4dbf3541e5ae6de394cc97")
]
}
I need to change the lvl value to anywhere between 0 and 5, not necessarily inregular increments. To do this, the parent object name is passed by a variable, thererfor I'm trying to build a dynamic query for a badge lvl to be changed.
Currently the end of my Flask route looks as so:
badge = quiz['badge']
C_user = current_user.get_id()
update = bdd.user.update({"_id": C_user, "badges": { '$elemMatch': {badges : badge}}}, {"$set": { "badges.$.lvl": 3 }}, upsert = True)
But with a query of this sort, I receive an error saying:
pymongo.errors.WriteError: The positional operator did not find the match needed from the query.
Am I totally wrong thinking I need to use the $elemMatch operator? Is there a way to dynamically target the different badges to increment the inner lvl?
Thank you!
Note: I first attempted this with MongoEngine but it seemed impossible to drill down with it.
you cannot do this, badges is an document, not an array. try
...
{"$set": { "badges.badge.lvl": 3 }}
...
But you'll have to explicitely do this for each badges fields.
The way to use your query is to update your scheme in something like
...
"badges" : [
{name:"cnc",
"lvl" : "0"
},
{"name":"laser",
"lvl" : "0"},
...
]
...
EDIT : with some updates on your scheme, here's a way to reach your goal :
New datata Schema :
{
"_id" : ObjectId("5b4dbf3541e5ae6de394cc99"),
"active" : true,
"email" : "admin#something.eu",
"password" : "$2b$12$5qqD3ZulKI7S6j.Wx513POpCNWRMppE.vY4.3EIZedm109VUPqXoi",
"badges" : [
{
"name" : "cnc",
"lvl" : "0"
},
{
"name" : "laser",
"lvl" : "5"
},
{
"name" : "impression3d",
"lvl" : "0"
},
{
"name" : "maker",
"lvl" : "0"
},
{
"name" : "electronique",
"lvl" : "0"
},
{
"name" : "badge",
"lvl" : 1
}
],
"roles" : [
ObjectId("5b4dbf3541e5ae6de394cc97")
]
}
and here an update query :
db['02'].update(
{_id: ObjectId("5b4dbf3541e5ae6de394cc99")}, // <= here you filter your documents
{ $set: { "badges.$[elem].lvl": "10" } }, // <= use of positionnal operator with identifier, needed for arrayFilters. elem is arbitraty
{ arrayFilters: [ { "elem.name": "laser" } ], // <= update only elements in array that match arrayFilters.
upsert: true }
)

Mongo aggregation, project a subfield of the first element in the array

I have a sub collection of elements and I want to project a certain subfield of the FIRST item in this collection. I have the following but it only projects the field for ALL elements in the array.
Items is the subcollection of Orders and each Item object has a Details sub object and an ItemName below that. I want to only return the item name of the FIRST item in the list. This returns the item name of every item in the list.
How can I tweak it?
db.getCollection('Orders').aggregate
([
{ $match : { "Instructions.1" : { $exists : true }}},
{ $project: {
_id:0,
'UserId': '$User.EntityId',
'ItemName': '$Items.Details.ItemName'
}
}
]);
Updated:
{
"_id" : "order-666156",
"State" : "ValidationFailed",
"LastUpdated" : {
"DateTime" : ISODate("2017-09-26T08:54:16.241Z"),
"Ticks" : NumberLong(636420128562417375)
},
"SourceOrderId" : "666156",
"User" : {
"EntityId" : NumberLong(34450),
"Name" : "Bill Baker",
"Country" : "United States",
"Region" : "North America",
"CountryISOCode" : "US",
},
"Region" : null,
"Currency" : null,
"Items" : [
{
"ClientOrderId" : "18740113",
"OrigClientOrderId" : "18740113",
"Quantity" : NumberDecimal("7487.0"),
"TransactDateTime" : {
"DateTime" : Date(-62135596800000),
"Ticks" : NumberLong(0)
},
"Text" : null,
"LocateRequired" : false,
"Details" : {
"ItemName" : "Test Item 1",
"ItemCost" : 1495.20
}
},
{
"ClientOrderId" : "18740116",
"OrigClientOrderId" : "18740116",
"Quantity" : NumberDecimal("241.0"),
"TransactDateTime" : {
"DateTime" : Date(-62135596800000),
"Ticks" : NumberLong(0)
},
"Text" : null,
"LocateRequired" : false,
"Details" : {
"ItemName" : "Test Item 2",
"ItemCost" : 2152.64
}
}
]
}
In case your are using at least MongoDB v3.2 you can use the $arrayElemAt operator for that. The below query does what you want. It will, however, not return any data for the sample you provided because the "Instructions.1": { $exists: true } filter removes the sample document.
db.getCollection('Orders').aggregate([{
$match: {
"Instructions.1": {
$exists: true
}
}
}, {
$project: {
"_id": 0,
"UserId": "$User.EntityId",
"ItemName": { $arrayElemAt: [ "$Items.Details.ItemName", 0 /* first item! */] }
}
}])

MongodDB retrieve a subdocument by the property name

My document looks like:
{ "entity_id" : 2,
"features" :
[
{ "10" : "name" },
{ "20" : "description" },
... ,
{ "90" : "availability" }
]
}
I would like to, knowing 2 things: the value of "entity_id" (2), and the value of the property in one of the elements of the "features" array, to retrieve only the subdocument
{ "20" : "description" }
in one query. Many thanks!
If you want to get only a part of the whole document, use so called Projection operators
See examples below:
> db.collection.find().pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"entity_id" : 2,
"features" : [
{
"10" : "name"
},
{
"20" : "description"
},
{
"90" : "availability"
}
]
}
Projection operators are specified in find() like here:
> db.collection.find({},{ features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"features" : [
{
"20" : "description"
}
]
}
> db.collection.find({},{ entity_id : 1, features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"entity_id" : 2,
"features" : [
{
"20" : "description"
}
]
}
> db.collection.find({},{ _id : 0, entity_id : 1, features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{ "entity_id" : 2, "features" : [ { "20" : "description" } ] }
$elemMatch for projection is available in MongoDB since version 2.2.
Hope it solves your problem.

Change value in nested document in MongoDB-array

I have document:
{
"_id" : ObjectId("5152ba0708c164359d5ca616"),
"info": "Some interesting information.",
"elements" : [
{
"status" : "accepted",
"id" : "10"
},
{
"status" : "waiting",
"id" : "20"
},
{
"status" : "accepted",
"id" : "30"
}
]
}
How can I modify "status" to "accepted" where "status" is "waiting" and "id" is 20?
use positional ("$") operator.
// single
db.collection.update({ _id: ..., "elements.id": "20" }, { $set: { "elements.$.status": "status" } })
// all
db.collection.update({ "elements.id": "20" }, { $set: { "elements.$.status": "status" } }, { multi: true })
You have to use elemMatch (http://docs.mongodb.org/manual/reference/projection/elemMatch/) . To match both the items of the array like status and id 20 , then you can use the $set command. The reason behind using elemMatch is that it will make sure that both the criteria is matched on the same subdocument before returning the result
db.CollectionName.update({"_id" : ObjectId("5152ba0708c164359d5ca616") , "elements" : { "$elemMatch : {"status" : "waiting" , "Id" : "20"}}},{ $set : {"elements.$.status" : "accepted"}});