Search code examples
javascriptnode.jsmongodbmongodb-nodejs-driver

$ positional update operator with upsert


Say I have a collection of shops:

[
  {
    "_id": 0,
    "items": [
      {"_id": 3, "price": 10}
    ]
  },
  {
    "_id": 1,
    "items": []
  },
  {
    "_id": 2
  }
]

I want to update the price of item 3 in a shop to 30, inserting a new item in the shop if no item exists in that shop and/or inserting a new shop if necessary:

  • shop 0: simply updates the item
  • shop 1: appends {"_id": 3, "price": 30} to shop 1's items
  • shop 2: sets shop 2's items to [{"_id": 3, "price": 30}]
  • shop 3: inserts the document {"_id": 3, "items": [{"_id": 3, "price": 30}]}

In other terms, I want to:

  • collection.updateOne({_id: <shopId>, 'items._id': 3}, {$set: {'items.$.price': 30}}) if the item exists (shop 0)
  • collection.updateOne({_id: <shopId>}, {$push: {items: {_id: 3, price: 30}}}, {upsert: true}) if it doesn't (shops 1 to 3)

This is kind of like $ with upsert, but the documentation clearly states that upsert cannot be used with it:

Do not use the positional operator $ with upsert operations because inserts will use the $ as a field name in the inserted document.

$[<identifier>] doesn't work either:

If the upsert operation did not include an exact equality match and no matching documents were found to update, the upsert operation would error.

Is there a way to do this without having to hit the database multiple times?


Things I've tried:

// Updating the path 'items' would create a conflict at 'items'
collection.updateOne(
  {_id: 0, 'items._id': 3},
  {$set: {'items.$.price': 30}, $setOnInsert: {items: []}}
)

// $[<identifier>] with upsert doesn't work
// The path 'items' must exist in the document in order to apply array updates.
collection.updateOne(
  {_id: 2},
  {$set: {'items.$[item].price': 30}},
  {arrayFilters: [{'item._id': 3}], upsert: true}
)

// Updating the path 'items' would create a conflict at 'items'
collection.updateOne(
  {_id: 0},
  {$set: {'items.$[item].price': 30}, $setOnInsert: {items: []}},
  {arrayFilters: [{'item._id': 3}], upsert: true}
)

Solution

  • As D. SM said, you can use bulkWrite to do multiple operations in one go:

    const shopId = 0
    collection.bulkWrite([
      // Remove any existing item
      {
        updateOne: {
          filter: {_id: shopId},
          update: {$pull: {items: {_id: 3}}}
        }
      },
      // Add the item with the updated price to the shop
      {
        updateOne: {
          filter: {_id: shopId},
          update: {
            $push: {items: {_id: 3, price: 30}}
          },
          upsert: true
        }
      }
    ])