skip to Main Content

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

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search