I have a problem while updating an array.
sample document :
_id:11,
marksObtained:[{physics:10}, {chemistry:12}, {maths: 11}, {biology:9}, {social:9}]
name:"Someone"
field to update is:
[{chemistry:13},{zoology:12}]
So the new document looks like:
_id:11,
marksObtained:[{physics:10}, {chemistry:13}, {maths: 11}, {biology:9}, {social:9},{zoology:12}]
name:"Someone"
marks in chemistry is updated to 13 and rest values are kept as it is
I have tried $push, $addToSet, update, updateOne,updateMany
I want to avoid writing code that iterates and compare labels.
Key and value are coming from device which are dynamic, so i want to update the keys which are already present in array and if new key comes it must appended in the array
CodePudding user response:
This is actually a pain. But there are a few options:
1 - Bulk operation
Your actions are count be simply a $pull
(to remove the existing value from the array, IF it exist) and $push
(add the correct value). You could do this through the bulk operations api to ensure that the writes are as atomic as possible:
const bulkOperation = db.students.initializeOrderedBulkOp()
bulkOperation.find({_id:11}).updateOne({$pull: {marksObtained: { chemistry: { $exists: true } } } })
bulkOperation.find({_id:11}).updateOne({$push: {marksObtained: { chemistry: 12 } } })
bulkOperation.execute()
eg: Dynamically in code:
const _idToUpdate = 11
const updateFields = [{ chemistry: 13 }, { zoology: 12 }]
const bulkOperation = db.collection('students').initializeOrderedBulkOp()
for (const field of updateFields) {
for (const key in field) {
const value = field[key]
const pullOp = { $pull: { marksObtained: { } } }
pullOp.$pull.marksObtained[key] = { $exists: true }
const pushOp = { $push: { marksObtained: { } } }
pushOp.$push.marksObtained[key] = value
console.log('name', key, 'value', value, 'pullOp', JSON.stringify(pullOp), 'pushOp', JSON.stringify(pushOp))
bulkOperation.find({ _id: _idToUpdate }).updateOne(pullOp)
bulkOperation.find({ _id: _idToUpdate }).updateOne(pushOp)
}
}
bulkOperation.execute()
2 - Custom $function
to run javascript to set the values
As per this playground: https://mongoplayground.net/p/PcV6dMoyJ6Y.
You can set a custom javascript function to do the dirty work for you.
Annoyingly in playground, it doesn't format very well, hence it being on a single line, but an example is this:
db.collection.update({
_id: 11
},
[
{
$set: {
marksObtained: {
$function: {
body: "function(marks) { const updateFields = { chemistry: 13, zoology: 12, french: 11 }; for (const updateFieldName in updateFields) { let updated = false;for (const markObtained of marks) { for (const markObtainedKey in markObtained) { if (markObtainedKey === updateFieldName) { markObtained[markObtainedKey] = updateFields[updateFieldName]; updated = true; } } } if (!updated) { const newTopic = {}; newTopic[updateFieldName] = updateFields[updateFieldName]; marks.push(newTopic); } } return marks }",
args: [
"$marksObtained"
],
lang: "js"
}
}
}
}
])
Where the function is:
const updateFields = { chemistry: 13, zoology: 12, french: 11 }
for (const updateFieldName in updateFields) {
let updated = false
for (const markObtained of marks) {
for (const markObtainedKey in markObtained) {
if (markObtainedKey === updateFieldName) {
markObtained[markObtainedKey] = updateFields[updateFieldName]
updated = true
}
}
}
if (!updated) {
const newTopic = {}
newTopic[updateFieldName] = updateFields[updateFieldName]
marks.push(newTopic)
}
}
return marks
Obviously you'll have to set the values as per a single object as I've done, but you adjust as you wish.
Note: It would be a lot easier if you either formatted your marksObtained
as:
marksObtained: {
physics: 10,
chemistry: 12,
maths: 11,
biology: 9,
social: 9
}
or
marksObtained: [
{lesson: 'physics', score: 10},
{lesson: 'chemistry', score: 12},
{lesson: 'maths', score: 11},
{lesson: 'biology', score: 9},
{lesson: 'social', score: 9}
]
Having an array with a single object doesn't really make sense.
CodePudding user response:
It can be done in one update query. If the order of the items in the array is important, it is a bit clumsy. Otherwise it can be elegant:
order of the items is important:
db.collection.updateMany(
{_id: 11},
[
{$set: {
marksObtained: {
$map: {
input: "$marksObtained",
in: {$first: {$objectToArray: "$$this"}}
}
},
dataToUpdate: {
$map: {
input: [
{chemistry: 13},
{zoology: 12}
],
in: {$first: {$objectToArray: "$$this"}}
}
}
}},
{$set: {
updateData: {
$filter: {
input: "$dataToUpdate",
cond: {$in: ["$$this.k", "$marksObtained.k"]}
}
},
newData: {
$filter: {
input: "$dataToUpdate",
cond: {$not: {$in: ["$$this.k", "$marksObtained.k"]}}
}
},
dataToUpdate: "$$REMOVE"
}},
{$set: {
marksObtained: {
$concatArrays: [
{
$map: {
input: "$marksObtained",
in: {
$cond: [
{$in: ["$$this.k", "$updateData.k"]},
{$arrayElemAt:
["$updateData", {$indexOfArray: ["$updateData.k", "$$this.k"]}]
},
"$$this"
]
}
}
},
"$newData"
]
},
newData: "$$REMOVE",
updateData: "$$REMOVE"
}
},
{$set: {
marksObtained: {
$map: {input: "$marksObtained", in: ["$$this"]}
}
}},
{$set: {
marksObtained: {
$map: {
input: "$marksObtained",
in: {$arrayToObject: "$$this"}
}
}
}}
])
See how it works on the playground example
order of the items is NOT important:
db.collection.updateMany(
{_id: 11},
[
{$set: {
marksObtained: {$map: {
input: "$marksObtained",
in: {$first: {$objectToArray: "$$this"}}
}},
dataToUpdate: {$map: {
input: [{chemistry: 13}, {zoology: 12}],
in: {$first: {$objectToArray: "$$this"}}
}}
}},
{$set: {
marksObtained: {$concatArrays: [
{$filter: {
input: "$marksObtained",
cond: {$not: {$in: ["$$this.k", "$dataToUpdate.k"]}}
}},
"$dataToUpdate"
]},
dataToUpdate: "$$REMOVE"
}},
{$set: {
marksObtained: {$map: {input: "$marksObtained", in: ["$$this"]}}
}},
{$set: {
marksObtained: {$map: {input: "$marksObtained", in: {$arrayToObject: "$$this"}}}
}}
])
See how it works on the playground example