16

I have such a schema:

doc:
{
    //Some fields
    visits:
    [
        {
            userID: Int32
            time: Int64
        }
    ]

}

I want to first check if a specific userID exists, if not, push a document with that userID and system time, else just update time value. I know neither $push nor $addToSet are not able to do that. Also using $ with upsert:true doesn't work, because of official documentation advice which says DB will use $ as field name instead of operator when trying to upsert.

Please guide me about this. Thanks

3
  • See if this helps. stackoverflow.com/a/41113069/2683814 Commented Dec 24, 2016 at 19:06
  • Thanks. It's a two step query. Can't this be done only in one query? Commented Dec 24, 2016 at 19:30
  • 1
    That's the only way I think it's possible. For top level attribute you can do it one query just as docs suggest. Commented Dec 24, 2016 at 20:33

3 Answers 3

14

You can use $addToSet to add an item to the array and $set to update an existing item in this array.

The following will add a new item to the array if the userID is not found in the array :

db.doc.update({
    visits: {
        "$not": {
            "$elemMatch": {
                "userID": 4
            }
        }
    }
}, {
    $addToSet: {
        visits: {
            "userID": 4,
            "time": 1482607614
        }
    }
}, { multi: true });

The following will update the subdocument array item if it matches the userId :

db.doc.update({ "visits.userID": 2 }, {
    $set: {
        "visits.$.time": 1482607614
    }
}, { multi: true });
Sign up to request clarification or add additional context in comments.

3 Comments

But here we have to execute two queries. Is there a solution with single query?
That's a shame that such basic functionality has to be done in two queries
I agree with Dominic. It might create concurrency issues.
0
const p = await Transaction.findOneAndUpdate(
            {
              _id: data.id,
              'products.id': { $nin: [product.id] },
            },
            {
              $inc: {
                actualCost: product.mrp,
              },
              $push: {
                products: { id: product.id },
              },
            },
            { new: true }
          );

or

db.collection.aggregate([
  {
    "$match": {
      "_id": 1
    }
  },
  {
    "$match": {
      "sizes.id": {
        "$nin": [
          7
        ]
      }
    }
  },
  {
    "$set": {
      "price": 20
    }
  }
])

https://mongoplayground.net/p/BguFa6E9Tra

Comments

0

I know it's very late. But it may help others. Starting from mongo4.4, we can use $function to use a custom function to implement our own logic. Also, we can use the bulk operation to achieve this output.

Assuming the existing data is as below

{
  "_id" : ObjectId("62de4e31daa9b8acd56656ba"),
  "entrance" : "Entrance1",
  "visits" : [
    {
            "userId" : 1,
            "time" : 1658736074
    },
    {
            "userId" : 2,
            "time" : 1658736671
    }
  ]
}

Solution 1: using custom function

db.visitors.updateMany(
  {_id: ObjectId('62de4e31daa9b8acd56656ba')},
  [
    {
      $set: {
        visits: {
          $function: {
            lang: "js",
            args: ["$visits"],
            body: function(visits) {
              let v = []
              let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
              if(Array.isArray(visits)) {
                v = visits.filter(x => x.userId != input.userId)
              }
              v.push(input)
              return v;
            }
          }
        }
      }
    }
  ]
)

In NodeJS, the function body should be enclosed with ` character

...
  lang: 'js',
  args: ["$visits"],
  body: `function(visits) {
    let v = []
    let input = {userId: 3, time: Math.floor(Date.now() / 1000)};
    if(Array.isArray(visits)) {
      v = visits.filter(x => x.userId != input.userId)
    }
    v.push(input)
    return v;
  }`
...

Solution 2: Using bulk operation:

Please note that the time here will be in the ISODate

var bulkOp = db.visitors.initializeOrderedBulkOp()
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$pull: { visits: { userId: 2 }} });
bulkOp.find({ _id: ObjectId('62de4e31daa9b8acd56656ba') }).updateOne({$push: {visits: {userId: 2, time: new Date()}}})
bulkOp.execute()

Reference link

Comments

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.