60

I have the following collection

{
    "_id" : ObjectId("57315ba4846dd82425ca2408"),
    "myarray" : [ 
        {
            userId : ObjectId("570ca5e48dbe673802c2d035"),
            point : 5
        },
        {
            userId : ObjectId("613ca5e48dbe673802c2d521"),
            point : 2
        },        
     ]
}

These are my questions

I want to push into myarray if userId doesn't exist, it should be appended to myarray. If userId exists, it should be updated to point.

I found this

db.collection.update({
    _id : ObjectId("57315ba4846dd82425ca2408"),
    "myarray.userId" :  ObjectId("570ca5e48dbe673802c2d035")
}, {
    $set: { "myarray.$.point": 10 }
})

But if userId doesn't exist, nothing happens.

and

db.collection.update({
    _id : ObjectId("57315ba4846dd82425ca2408")
}, {
  $push: {
      "myarray": {
          userId: ObjectId("570ca5e48dbe673802c2d035"),
          point: 10
      }
  }
})

But if userId object already exists, it will push again.

What is the best way to do this in MongoDB?

1
  • For reference, I added my verbose solution to the answer at the following link that covers the same scenario, ie how to add a new object to an array of objects unless a specific object value (eg userId) exists, in which case update the object: stackoverflow.com/a/52671119 Commented Nov 3, 2018 at 12:31

9 Answers 9

36

Try this

db.collection.update(
    { _id : ObjectId("57315ba4846dd82425ca2408")},
    { $pull: {"myarray.userId": ObjectId("570ca5e48dbe673802c2d035")}}
)
db.collection.update(
    { _id : ObjectId("57315ba4846dd82425ca2408")},
    { $push: {"myarray": {
        userId:ObjectId("570ca5e48dbe673802c2d035"),
        point: 10
    }}
)

Explanation:
In the first statement $pull removes the element with userId= ObjectId("570ca5e48dbe673802c2d035") from the array on the document where _id = ObjectId("57315ba4846dd82425ca2408").

In the second one, $push inserts this object { userId:ObjectId("570ca5e48dbe673802c2d035"), point: 10 } in the same array.

Sign up to request clarification or add additional context in comments.

3 Comments

would be great to explicit what your code does for future readers
ok , that is greate but in MongoDb the id should inserted automatically whereas you added it manually
two different cals, a great ideea but expensive.
35

The accepted answer by Flying Fisher is that the existing record will first be deleted, and then it will be pushed again.

A safer approach (common sense) would be to try to update the record first, and if that did not find a match, insert it, like so:

// first try to overwrite existing value
var result = db.collection.update(
   {
       _id : ObjectId("57315ba4846dd82425ca2408"),
       "myarray.userId": ObjectId("570ca5e48dbe673802c2d035")
   },
   {
       $set: {"myarray.$.point": {point: 10}}
   }
);
// you probably need to modify the following if-statement to some async callback
// checking depending on your server-side code and mongodb-driver
if(!result.nMatched)
{
    // record not found, so create a new entry
    // this can be done using $addToSet:
    db.collection.update(
        {
            _id: ObjectId("57315ba4846dd82425ca2408")
        },
        {
            $addToSet: {
                myarray: {
                    userId: ObjectId("570ca5e48dbe673802c2d035"),
                    point: 10
                }
            }
        }
    );
    // OR (the equivalent) using $push:
    db.collection.update(
        {
            _id: ObjectId("57315ba4846dd82425ca2408"),
            "myarray.userId": {$ne: ObjectId("570ca5e48dbe673802c2d035"}}
        },
        {
            $push: {
                myarray: {
                    userId: ObjectId("570ca5e48dbe673802c2d035"),
                    point: 10
                }
            }
        }
    );
}

This should also give (common sense, untested) an increase in performance, if in most cases the record already exists, only the first query will be executed.

2 Comments

What happens if two concurrent requests enter simultaneously into the if-statements? only one of two $addToSet has effect, hasn't it?
@FabioFormosa Yeah, but you're right, this is still unsafe. What if meanwhile, not only $addToSet is executed, but also the points are updated... This code should definitely be in a transaction.
23

There is a option called update documents with aggregation pipeline starting from MongoDB v4.2,

  • check condition $cond if userId in myarray.userId or not
  • if yes then $map to iterate loop of myarray array and check condition if userId match then merge with new document using $mergeObjects
  • if no then $concatArrays to concat new object and myarray
let _id = ObjectId("57315ba4846dd82425ca2408");
let updateDoc = {
  userId: ObjectId("570ca5e48dbe673802c2d035"),
  point: 10
};

db.collection.update(
  { _id: _id },
  [{
    $set: {
      myarray: {
        $cond: [
          { $in: [updateDoc.userId, "$myarray.userId"] },
          {
            $map: {
              input: "$myarray",
              in: {
                $mergeObjects: [
                  "$$this",
                  {
                    $cond: [
                      { $eq: ["$$this.userId", updateDoc.userId] },
                      updateDoc,
                      {}
                    ]
                  }
                ]
              }
            }
          },
          { $concatArrays: ["$myarray", [updateDoc]] }
        ]
      }
    }
  }]
)

Playground

2 Comments

That's a great way to solve that. I tried to "improve" it by first setting the index of the element to be updated and using that avoid going through the array multiple times. But I can't figure out how to use that index to only update the right element like here. So I'll go with your solution!
crazy lengthy, is it necessary?...
4

I haven't found any solutions based on a one atomic query. Instead there are 3 ways based on a sequence of two queries:

  1. always $pull (to remove the item from array), then $push (to add the updated item to array)

    db.collection.update(
                   { _id : ObjectId("57315ba4846dd82425ca2408")},
                   { $pull: {"myarray.userId": ObjectId("570ca5e48dbe673802c2d035")}}
    )
    
    db.collection.update(
                   { _id : ObjectId("57315ba4846dd82425ca2408")},
                   {
                     $push: {
                              "myarray": {
                                          userId:ObjectId("570ca5e48dbe673802c2d035"),
                                          point: 10
                                         }
                             }
                    }
    )
    
  2. try to $set (to update the item in array if exists), then get the result and check if the updating operation successed or if a $push needs (to insert the item)

    var result = db.collection.update(
        {
           _id : ObjectId("57315ba4846dd82425ca2408"),
          "myarray.userId": ObjectId("570ca5e48dbe673802c2d035")
        },
        {
           $set: {"myarray.$.point": {point: 10}}
        }
     );
    
    if(!result.nMatched){
           db.collection.update({_id: ObjectId("57315ba4846dd82425ca2408")},
                                {
                                  $addToSet: {
                                               myarray: {
                                                  userId: ObjectId("570ca5e48dbe673802c2d035"),
                                                  point: 10
                                              }
                                }
           );
    
  3. always $addToSet (to add the item if not exists), then always $set to update the item in array

       db.collection.update({_id: ObjectId("57315ba4846dd82425ca2408")},
                             myarray: { $not: { $elemMatch: {userId: ObjectId("570ca5e48dbe673802c2d035")} } } },
                            { 
                               $addToSet : {
                                             myarray: {
                                                        userId: ObjectId("570ca5e48dbe673802c2d035"),
                                                        point: 10
                                                       }
                                            }
                             },
                            { multi: false, upsert: false});
    
       db.collection.update({
                              _id: ObjectId("57315ba4846dd82425ca2408"),
                               "myArray.userId": ObjectId("570ca5e48dbe673802c2d035")
                            },
                            { $set : { myArray.$.point: 10 } },
                            { multi: false, upsert: false});
    

1st and 2nd way are unsafe, so transaction must be established to avoid two concurrent requests could push the same item generating a duplicate.

3rd way is safer. the $addToSet adds only if the item doesn't exist, otherwise nothing happens. In case of two concurrent requests, only one of them adds the missing item to the array.

8 Comments

How is approach #3 safe? If a concurrent request has modified the array element so that points is no longer 10 then the first call will add a second element for the same userId to the array.
The only race condition would be: the element is not in the array, the first request completes the $addToSet, now the second request runs both updates and finally the first request runs the $set. There will be always one only element (never two!) because only one $addToSet hits! The value of the point field will be the value of the last request that executes the $set. You shouldn't interested to which request executes the $addToSet . So it's a safer way because it doesn't fall in a double insert.
If an array element exists for the same userId but with a different point value, then a second element for the same userId would be added to the array. The $addToSet approach would only work if 10 is forever the only possible value for point which is unlikely.
Ah, now I understand what you mean. I have missed the $elemMatch because I didn't scroll far enough. In that case you're right - it should work. You can even use $push instead of $addToSet because you've basically covered the set behavior by the $elemMatch filter.
I've just re-edited that line to avoid the horinzontal scroll that caused the misunderstanding. It could be useful to other people. Thank you a lot for the feedback.
|
3

Unfortunately "upsert" operation is not possible on embedded array. Operators simply do not exist so that this is not possible in a single statement.Hence you must perform two update operations in order to do what you want. Also the order of application for these two updates is important to get desired result.

Comments

1

Possible solution with aggregation pipeline:

db.collection.update(
      { _id },
      [
        {
          $set: {
            myarray: { $filter: {
              input: '$myarray',
              as: 'myarray',
              cond: { $ne: ['$$myarray.userId', ObjectId('570ca5e48dbe673802c2d035')] },
            } },
          },
        },
        {
          $set: {
            myarray: {
              $concatArrays: [
                '$myarray',
                [{ userId: ObjectId('570ca5e48dbe673802c2d035'), point: 10 },
                ],
              ],
            },
          },
        },
      ],
    );

We use 2 stages:

  1. filter myarray (= remove element if userId exist)
  2. concat filtered myarray with new element;

Comments

0

When you want update or insert value in array try it

Object in db

key:name,
key1:name1,
arr:[
    {
        val:1,
        val2:1
    }
]

Query

var query = {
    $inc:{
        "arr.0.val": 2,
        "arr.0.val2": 2
    }
}
.updateOne( { "key": name }, query, { upsert: true }

key:name,
key1:name1,
arr:[
    {
        val:3,
        val2:3
    }
]

Comments

-1

array update and create don't mix in under one query, if you care much about atomicity then there's this solution:

normalise your schema to,

{
    "_id" : ObjectId("57315ba4846dd82425ca2408"),
    userId : ObjectId("570ca5e48dbe673802c2d035"),
    point : 5
}

1 Comment

creating or updating this schema document is possible in one query
-2

You could use a variation of the .forEach/.updateOne method I currently use in mongosh CLI to do things like that. In the .forEach, you might be able to set all of your if/then conditions that you mentioned.

Example of .forEach/.updateOne:

let medications = db.medications.aggregate([
  {$match: {patient_id: {$exists: true}}}
]).toArray();

medications.forEach(med => {
  try {
    db.patients.updateOne({patient_id: med.patient_id}, 
      {$push: {medications: med}}
    )
  } catch {
    console.log("Didn't find match for patient_id.  Could not add this med to a patient.")
  }  
})

This may not be the most "MongoDB way" to do it, but it definitely works and gives you the freedom of javascript to do things within the .forEach.

2 Comments

This doesn't answer the question, and is very non-idiomatic (why use an aggregation for a simple query in the first lines?). If you're going to use an aggregation, it'd be better to aggregate over the entire collection, perform your updates inline, and then $merge the output with the original collection. But as others have mentioned, the best solution is probably the update + aggregation pipeline syntax from MongoDB 4.2+. Also, it'd be better to use .forEach instead of .toArray on the initial query if you did it this way.
Thanks for the info. Definitely had and still do have a lot to learn with MongoDB operations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.