MongoDB and Flask - Updating objects in a document's array (nested updating) - mongodb

Assume we have following collection.
{
"Region":"Karnataka",
"ShortCode":"KA",
"SubRegion":[
{
"District":"Banglore",
"Commodity":[
{
"Name":"items",
"isActive":true,
"CommoditySubType":[
{
"Title":"Moistouriser",
"isActive":true,
"hasGrades":true,
"Grade":[
{
"Title":"Premium",
"Rate":"150",
"isActive":true,
"hasRates":true,
"hasSizes":true,
"StartDate":"2021-03-31",
"EndDate":"2021-04-06",
"StartTime":"9:00am",
"EndTime":"6:00pm",
"Sizes":[
{
"Title":"Small",
"isActive":true
}
]
"LastSevenDaysDates":[{
"Date":2021-03-31,
"Price":"150"
}]
}
]
}
]
}
]
},
{
"District":"Coorg",
"Commodity":[]
}
] }
I want to update an object under LastsevndayDates. I tried this.
mongo.db.supplierDailyPrice.update(
{
"Region":region,
"SubRegion.District":district,
"SubRegion.Commodity.Name":commodity,
"SubRegion.Commodity.CommoditySubType.Title":commoditysubtype,
"$and": [
{ "SubRegion.Commodity.CommoditySubType.Grade": { "$exists": True}},
{ "SubRegion.Commodity.CommoditySubType.Grade.Title": "Premium" },
]
},
{
"$set": {
"SubRegion.$[].Commodity.$[].CommoditySubType.$[].Grade.$[].LastSevenDaysDates": Date
}
})
But it's failing with the error pymongo.errors.WriteError: The path 'SubRegion.0.Commodity.0.CommoditySubType.1.Grade' must exist in the document in order to apply array updates.

You can use arrayFilters to solve your problem
db.supplierDailyPrice.update({
"Region":region
},
{
$set:{
"Subregion.$[sub].Commodity.$[com].CommoditySubType.$[Cst].Grade.$[grd]. LastSevenDaysDates.$[sev].date": Date
}
},
{
array_filter : [
{"sub.district": district},
{"com.name": commodity},
{"Cst.title": commoditysubtype},
{"grd.title": "premium"},
{"sev.price":"150"},
]
})
Not tested the code but something like this will solve your problem. For more reference you can check out the links below:
Update deeply nested array in mongodb
update deeply nested array mongodb
Pymongo error for ArrayFilters to update multiple subdocuments

Ujjwal's answer is on the right track. I fixed a few typos.
More information here about $[] usage.
https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/
https://developer.mongodb.com/how-to/update-array-elements-document-mql-positional-operators/
db.supplierDailyPrice.update_one({
"Region": region
},
{
$set:{
"SubRegion.$[sub].Commodity.$[com].CommoditySubType.$[cst].Grade.$[grd].LastSevenDaysDates.$[sev].Date": "2021-04-03"
}
},
{
"array_filters" : [
{"sub.District": district},
{"com.Name": commodity},
{"cst.Title": commoditysubtype},
{"grd.Title": grade},
{"sev.Price":"150"},
]
},
{
upsert=true
}
)

Related

find only matching elements from a JSON array in Ne DB/react native-local-mongo/mongo db

i am trying to read only matching keys from a JSON with a nested array.
here is my JSON.
data:[
{
"locale":"en_US",
"pages":[
{
pageName:"testpage1",
"messages":{
"m1":"v1",
"m2":""
}
},
{
pageName:"testpage2",
"messages":{
"m1":"v3",
"m2":"v4"
}
}
]
},
{
"locale":"in_L1",
"pages":[
{
pageName:"testpage1",
"messages":{
"m1":"i1",
"m2":"i2"
}
},
{
pageName:"testpage2",
"messages":{
"m1":"i3",
"m2":"i4"
}
}
]
}
]
I am trying the below query:
db['collec1'].find({locale:"en_US", pages:{$elemMatch:{pageName:"testpage1"},
{locale:1,"pages.pageName":1}}})
also tried,
db['collec1'].find({locale:"en_US", "pages.pageName":"testpage1"},{locale:1,"pages.pageName":1}}})
both are returning, the all the elements in the array as below:
[
{
"locale":"en_US",
pages:{pageName:["testpage1", "testpage2"]}]
}
]
Expected output is:
[
{
"locale":"en_US",
pages:{pageName:["testpage1"]}]
}
]
can someone help me where i am doing it wrong and what needs to be changed?
i am using this library which is a clone of mongodb called Ne DB.
db['collec1'].find({locale:"en_US", pages:{$elemMatch:{pageName:"testpage1"}}).
try with an empty project in the find method {} and then try like
the above example

Update specific element in nested array

I have the following structure
{
"_id":"5b609d1a5b4e2c03e512992b",
"files_missing":5,
"files_rejected":0,
"userId":"5afcbbcf26d784ca35fac580",
"files":[
{
"status":"sent",
"_id":"5b609d1a5b4e2c03e5129930",
"fileId":"5afcbad526d784ca35fac576",
"urls" [
{
"_id":"5b60e10114bce7084cebc1da",
"file":"/data/5b609d195b4e2c03e512992a.pdf",
"status":"sent"
},{
"_id":"5b60e1ea14bce7084cebc1dd",
"file":"/data/5b60e1ea14bce7084cebc1dd.pdf",
"status":"sent"
}
]
}
],
"contactId":"5b609d195b4e2c03e512992a",
"created_at":"2018-07-31T17:32:10.251Z",
"updated_at":"2018-07-31T17:32:10.251Z",
}
And I want to update the status inside in one of the elements of urls
I tried:
db.collection.update({contactId:"5b609d195b4e2c03e512992a",
'files.urls._id': "5b60e1ea14bce7084cebc1dd"},{$set:
{'files.urls.$.status': 'rejected'}})
MongoDB version: 3.2.17

MongoDB conditionally $addToSet sub-document in array by specific field

Is there a way to conditionally $addToSet based on a specific key field in a subdocument on an array?
Here's an example of what I mean - given the collection produced by the following sample bootstrap;
cls
db.so.remove();
db.so.insert({
"Name": "fruitBowl",
"pfms" : [
{
"n" : "apples"
}
]
});
n defines a unique document key. I only want one entry with the same n value in the array at any one time. So I want to be able to update the pfms array using n so that I end up with just this;
{
"Name": "fruitBowl",
"pfms" : [
{
"n" : "apples",
"mState": 1111234
}
]
}
Here's where I am at the moment;
db.so.update({
"Name": "fruitBowl",
},{
// not allowed to do this of course
// "$pull": {
// "pfms": { n: "apples" },
// },
"$addToSet": {
"pfms": {
"$each": [
{
"n": "apples",
"mState": 1111234
}
]
}
}
}
)
Unfortunately, this adds another array element;
db.so.find().toArray();
[
{
"Name" : "fruitBowl",
"_id" : ObjectId("53ecfef5baca2b1079b0f97c"),
"pfms" : [
{
"n" : "apples"
},
{
"n" : "apples",
"mState" : 1111234
}
]
}
]
I need to effectively upsert the apples document matching on n as the unique identifier and just set mState whether or not an entry already exists. It's a shame I can't do a $pull and $addToSet in the same document (I tried).
What I really need here is dictionary semantics, but that's not an option right now, nor is breaking out the document - can anyone come up with another way?
FWIW - the existing format is a result of language/driver serialization, I didn't choose it exactly.
further
I've gotten a little further in the case where I know the array element already exists I can do this;
db.so.update({
"Name": "fruitBowl",
"pfms.n": "apples",
},{
$set: {
"pfms.$.mState": 1111234,
},
}
)
But of course that only works;
for a single array element
as long as I know it exists
The first limitation isn't a disaster, but if I can't effectively upsert or combine $addToSet with the previous $set (which of course I can't) then it the only workarounds I can think of for now mean two DB round-trips.
The $addToSet operator of course requires that the "whole" document being "added to the set" is in fact unique, so you cannot change "part" of the document or otherwise consider it to be a "partial match".
You stumbled on to your best approach using $pull to remove any element with the "key" field that would result in "duplicates", but of course you cannot modify the same path in different update operators like that.
So the closest thing you will get is issuing separate operations but also doing that with the "Bulk Operations API" which is introduced with MongoDB 2.6. This allows both to be sent to the server at the same time for the closest thing to a "contiguous" operations list you will get:
var bulk = db.so.initializeOrderedBulkOp();
bulk.find({ "Name": "fruitBowl", "pfms.n": "apples": }).updateOne({
"$pull": { "pfms": { "n": "apples" } }
});
bulk.find({ "Name": "fruitBowl" }).updateOne({
"$push": { "pfms": { "n": "apples", "state": 1111234 } }
})
bulk.execute();
That pretty much is your best approach if it is not possible or practical to move the elements to another collection and rely on "upserts" and $set in order to have the same functionality but on a collection rather than array.
I have faced the exact same scenario. I was inserting and removing likes from a post.
What I did is, using mongoose findOneAndUpdate function (which is similar to update or findAndModify function in mongodb).
The key concept is
Insert when the field is not present
Delete when the field is present
The insert is
findOneAndUpdate({ _id: theId, 'likes.userId': { $ne: theUserId }},
{ $push: { likes: { userId: theUserId, createdAt: new Date() }}},
{ 'new': true }, function(err, post) { // do the needful });
The delete is
findOneAndUpdate({ _id: theId, 'likes.userId': theUserId},
{ $pull: { likes: { userId: theUserId }}},
{ 'new': true }, function(err, post) { // do the needful });
This makes the whole operation atomic and there are no duplicates with respect to the userId field.
I hope this helpes. If you have any query, feel free to ask.
As far as I know MongoDB now (from v 4.2) allows to use aggregation pipelines for updates.
More or less elegant way to make it work (according to the question) looks like the following:
db.runCommand({
update: "your-collection-name",
updates: [
{
q: {},
u: {
$set: {
"pfms.$[elem]": {
"n":"apples",
"mState": NumberInt(1111234)
}
}
},
arrayFilters: [
{
"elem.n": {
$eq: "apples"
}
}
],
multi: true
}
]
})
In my scenario, The data need to be init when not existed, and update the field If existed, and the data will not be deleted. If the datas have these states, you might want to try the following method.
// Mongoose, but mostly same as mongodb
// Update the tag to user, If there existed one.
const user = await UserModel.findOneAndUpdate(
{
user: userId,
'tags.name': tag_name,
},
{
$set: {
'tags.$.description': tag_description,
},
}
)
.lean()
.exec();
// Add a default tag to user
if (user == null) {
await UserModel.findOneAndUpdate(
{
user: userId,
},
{
$push: {
tags: new Tag({
name: tag_name,
description: tag_description,
}),
},
}
);
}
This is the most clean and fast method in the scenario.
As a business analyst , I had the same problem and hopefully I have a solution to this after hours of investigation.
// The customer document:
{
"id" : "1212",
"customerCodes" : [
{
"code" : "I"
},
{
"code" : "YK"
}
]
}
// The problem : I want to insert dateField "01.01.2016" to customer documents where customerCodes subdocument has a document with code "YK" but does not have dateField. The final document must be as follows :
{
"id" : "1212",
"customerCodes" : [
{
"code" : "I"
},
{
"code" : "YK" ,
"dateField" : "01.01.2016"
}
]
}
// The solution : the solution code is in three steps :
// PART 1 - Find the customers with customerCodes "YK" but without dateField
// PART 2 - Find the index of the subdocument with "YK" in customerCodes list.
// PART 3 - Insert the value into the document
// Here is the code
// PART 1
var myCursor = db.customers.find({ customerCodes:{$elemMatch:{code:"YK", dateField:{ $exists:false} }}});
// PART 2
myCursor.forEach(function(customer){
if(customer.customerCodes != null )
{
var size = customer.customerCodes.length;
if( size > 0 )
{
var iFoundTheIndexOfSubDocument= -1;
var index = 0;
customer.customerCodes.forEach( function(clazz)
{
if( clazz.code == "YK" && clazz.changeDate == null )
{
iFoundTheIndexOfSubDocument = index;
}
index++;
})
// PART 3
// What happens here is : If i found the indice of the
// "YK" subdocument, I create "updates" document which
// corresponds to the new data to be inserted`
//
if( iFoundTheIndexOfSubDocument != -1 )
{
var toSet = "customerCodes."+ iFoundTheIndexOfSubDocument +".dateField";
var updates = {};
updates[toSet] = "01.01.2016";
db.customers.update({ "id" : customer.id } , { $set: updates });
// This statement is actually interpreted like this :
// db.customers.update({ "id" : "1212" } ,{ $set: customerCodes.0.dateField : "01.01.2016" });
}
}
}
});
Have a nice day !

MongoDB: Too many positional (i.e. '$') elements found in path

I just upgraded to Mongo 2.6.1 and one update statement that was working before is not returning an error. The update statement is:
db.post.update( { 'answers.comments.name': 'jeff' },
{ '$set': {
'answers.$.comments.$.name': 'joe'
}},
{ multi: true }
)
The error I get is:
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 2,
"errmsg" : "Too many positional (i.e. '$') elements found in path 'answers.$.comments.$.createUsername'"
}
})
When I update an element just one level deep instead of two (i.e. answers.$.name instead of answers.$.comments.$.name), it works fine. If I downgrade my mongo instance below 2.6, it also works fine.
You CAN do this, you just need Mongo 3.6! Instead of redesigning your database, you could use the Array Filters feature in Mongo 3.6, which can be found here:
https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-array-filters
The beauty of this is that you can bind all matches in an array to a variable, and then reference that variable later. Here is the prime example from the link above:
Use arrayFilters.
MongoDB 3.5.12 extends all update modifiers to apply to all array
elements or all array elements that match a predicate, specified in a
new update option arrayFilters. This syntax also supports nested array
elements.
Let us assume a scenario-
"access": {
"projects": [{
"projectId": ObjectId(...),
"milestones": [{
"milestoneId": ObjectId(...),
"pulses": [{
"pulseId": ObjectId(...)
}]
}]
}]
}
Now if you want to add a pulse to a milestone which exists inside a project
db.users.updateOne({
"_id": ObjectId(userId)
}, {
"$push": {
"access.projects.$[i].milestones.$[j].pulses": ObjectId(pulseId)
}
}, {
arrayFilters: [{
"i.projectId": ObjectId(projectId)
}, {
"j.milestoneId": ObjectId(milestoneId)
}]
})
For PyMongo, use arrayFilters like this-
db.users.update_one({
"_id": ObjectId(userId)
}, {
"$push": {
"access.projects.$[i].milestones.$[j].pulses": ObjectId(pulseId)
}
}, array_filters = [{
"i.projectId": ObjectId(projectId)
}, {
"j.milestoneId": ObjectId(milestoneId)
}])
Also,
Each array filter must be a predicate over a document with a single
field name. Each array filter must be used in the update expression,
and each array filter identifier $[] must have a corresponding
array filter. must begin with a lowercase letter and not contain
any special characters. There must not be two array filters with the
same field name.
https://jira.mongodb.org/browse/SERVER-831
The positional operator can be used only once in a query. This is a limitation, there is an open ticket for improvement: https://jira.mongodb.org/browse/SERVER-831
As mentioned; more than one positional elements not supported for now. You may update with mongodb cursor.forEach() method.
db.post
.find({"answers.comments.name": "jeff"})
.forEach(function(post) {
if (post.answers) {
post.answers.forEach(function(answer) {
if (answer.comments) {
answer.comments.forEach(function(comment) {
if (comment.name === "jeff") {
comment.name = "joe";
}
});
}
});
db.post.save(post);
}
});
db.post.update(
{ 'answers.comments.name': 'jeff' },
{ '$set': {
'answers.$[i].comments.$.name': 'joe'
}},
{arrayFilters: [ { "i.comments.name": { $eq: 'jeff' } } ]}
)
check path after answers for get key path right
I have faced the same issue for the as array inside Array update require much performance impact. So, mongo db doest not support it. Redesign your database as shown in the given link below.
https://pythonolyk.wordpress.com/2016/01/17/mongodb-update-nested-array-using-positional-operator/
db.post.update( { 'answers.comments.name': 'jeff' },
{ '$set': {
'answers.$.comments.$.name': 'joe'
}},
{ multi: true }
)
Answer is
db.post.update( { 'answers.comments.name': 'jeff' },
{ '$set': {
'answers.0.comments.1.name': 'joe'
}},
{ multi: true }
)

mongodb update push sub-subdocument

The question is quite easy. I have the following document in mongoDb:
_id : ObjectId("519e3c14ade67153e9a003a0")
status:1,
name:"name",
details:
{
crm:115,
webs:
[
{ tag:"blog" , url:"http://..."}
],
contacts:
[
{
_id:ObjectId("009e37e1ade67153e9a0039e"),
name:"me",
phones:
[
{ tag:"home", number:"123..." },
{tag:"mobile", number:"123456789"}
]
}
]
}
I am trying to update one fo the phones in the subdocument details.contacts doing this:
db.new.update(
{
_id : ObjectId("519e3c14ade67153e9a003a0"),
"details.contacts._id": ObjectId("009e37e1ade67153e9a0039e"),
"details.contacts.phones.tag":"home"
},
{
$set:
{
"details.contacts.$.phones.$":
{
tag:"home",
number:"965923777"
}
}
})
and finally I get the following error: Cannot apply the positional operator without a corresponding query field containing an array.
There is anyway to update the document in this way or i should send all the phones to update this field?