Home > Net >  Update element of an array if and only if data for element is present in new document mongodb
Update element of an array if and only if data for element is present in new document mongodb

Time:12-08

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

  • Related