简体   繁体   中英

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

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM