Home > Blockchain >  How to only update one match of an array filter in mongodb
How to only update one match of an array filter in mongodb

Time:01-05

Problem:

I need to only update one document in the spots available array that has an id of "empty". My previous query was updating all matching sub documents with "empty" as the id; which is no good Example Below. So I decided to use aggregation so that I could add a limit stage so that I could only update one item, but come to find out I cannot update the original document with an aggregation. This leaves the only option to use an array filter that only updates one/first of its matches. Is this possible? I feel like there has to be a way to only update one match on an array filter and if there isn't this is definitely something that should be added.

My code:

This code updates every object with "empty"

        const client = await clientPromise;
            const db = client.db();
            // const query = db.collection('events').aggregate(agg);


            const query = await db.collection('events').updateOne({
                _id: new ObjectId("6398c34ca67dbe3286452f23"),
                createdBy: new ObjectId("636c1778f1d09191074f9690"),
                "weights.weight": 12
            },
                {
                    $set: {
                        "weights.$.spotsAvailable.$[el2]": {
                            "name": "Wayne Wrestler",
                            "userId": new ObjectId("636c1778f1d09191074f9690")
                        }
                    }
                },
                {
                    arrayFilters: [{ "el2": { "userId": "empty" } }]
                })

Example documents:

Event:

{
  "_id": {
    "$oid": "6398c34ca67dbe3286452f23"
  },
  "name": "test",
  "createdBy": {
    "$oid": "636c1778f1d09191074f9690"
  },
  "description": "testing",
  "date": {
    "$date": {
      "$numberLong": "1645488000000"
    }
  },
  "location": {
    "type": "Point",
    "coordinates": [
      0,
      0
    ]
  },
  "weights": [
    {
      "spotsAvailable": [
        {
          "name": "empty",
          "userId": "empty"
        },
        {
          "name": "empty",
          "userId": "empty"
        },
        {
          "name": "empty",
          "userId": "empty"
        }
      ],
      "weight": 12
    },
    {
      "spotsAvailable": [
        {
// only one of these should've been updated, but both were
          "name": "Wayne Wrestler",
          "userId": {
            "$oid": "636c1778f1d09191074f9690"
          }
        },
        {
          "name": "Wayne Wrestler",
          "userId": {
            "$oid": "636c1778f1d09191074f9690"
          }
        }
      ],
      "weight": 15
    }
  ],
  "eventApplicants": [
    {
      "userId": {
        "$oid": "636c1778f1d09191074f9690"
      },
      "name": "Wayne Wrestler",
      "weight": 12
    }
  ]
}

User:

{
  "_id": {
    "$oid": "636c1778f1d09191074f9690"
  },
  "name": "Wayne Wrestler",
  "email": "[email protected]",
  "image": "https://lh3.googleusercontent.com/a/ALm5wu32gXjDIRxncjjQA9I4Yl-sjFH5EWsTlmvdM_0kiw=s96-c",
  "emailVerified": {
    "$date": {
      "$numberLong": "1670864727212"
    }
  },
  "createdEvents": [
    {
      "createdEventName": "test",
      "createdEventDate": {
        "$date": {
          "$numberLong": "1645488000000"
        }
      },
      "createdEventDescription": "testing",
      "createdEventWeights": [
        {
          "weight": "12",
          "filled": [
            false,
            false,
            false
          ]
        },
        {
          "weight": "15",
          "filled": [
            false,
            false
          ]
        }
      ],
      "createdEventId": {
        "$oid": "6398c34ca67dbe3286452f23"
      }
    }
  ],
  "userSignedUpEvents": [],
  "availableWeights": [
    1,
    123
  ],
  "signedUpEvents": [
    {
      "eventId": {
        "$oid": "636c722f67642c30dc5ffc30"
      },
      "eventName": "Utah",
      "eventDate": {
        "$date": {
          "$numberLong": "1667913330000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "636c722f67642c30dc5ffc30"
      },
      "eventName": "Utah",
      "eventDate": {
        "$date": {
          "$numberLong": "1667913330000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "637ec484ac2d675b30590b47"
      },
      "eventName": "Maybe?",
      "eventDate": {
        "$date": {
          "$numberLong": "1672272000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "636c722f67642c30dc5ffc30"
      },
      "eventName": "Utah",
      "eventDate": {
        "$date": {
          "$numberLong": "1667913330000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "638d5274628db2a7bf61df49"
      },
      "eventName": "Eva's",
      "eventDate": {
        "$date": {
          "$numberLong": "1698019200000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "636c722f67642c30dc5ffc30"
      },
      "eventName": "Utah",
      "eventDate": {
        "$date": {
          "$numberLong": "1667913330000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398a922abb5c168ede595fb"
      },
      "eventName": "Nikko's event",
      "eventDate": {
        "$date": {
          "$numberLong": "1670976000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398a922abb5c168ede595fb"
      },
      "eventName": "Nikko's event",
      "eventDate": {
        "$date": {
          "$numberLong": "1670976000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398c34ca67dbe3286452f23"
      },
      "eventName": "test",
      "eventDate": {
        "$date": {
          "$numberLong": "1645488000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398c34ca67dbe3286452f23"
      },
      "eventName": "test",
      "eventDate": {
        "$date": {
          "$numberLong": "1645488000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398c34ca67dbe3286452f23"
      },
      "eventName": "test",
      "eventDate": {
        "$date": {
          "$numberLong": "1645488000000"
        }
      },
      "accepted": false
    },
    {
      "eventId": {
        "$oid": "6398c34ca67dbe3286452f23"
      },
      "eventName": "test",
      "eventDate": {
        "$date": {
          "$numberLong": "1645488000000"
        }
      },
      "accepted": false
    }
  ]
}

I have tried:

  1. Pluging in variables without the new ObjectId syntax
  2. Plugin in variables with the new ObjectId syntax
  3. Using the exact same hardcoded values that I got from copying the aggregation code from compass for the node driver

All of these either don't work or result in every subdocument with "empty" getting filled

CodePudding user response:

You can first $unwind the weights for easier processing first. Use $reduce to iterate through the weights.spotsAvailable array and use a compound object to store the result and a flag to indicate whether it is updated or not. Finally use the result to $merge back to the original document.

db.collection.aggregate([
  {
    $match: {
      "_id": ObjectId("6398c34ca67dbe3286452f23"),
      createdBy: ObjectId("636c1778f1d09191074f9690"),
      "weights.weight": 12,
      "weights.spotsAvailable.userId": "empty"
    }
  },
  {
    "$unwind": "$weights"
  },
  {
    "$addFields": {
      "results": {
        "$reduce": {
          "input": "$weights.spotsAvailable",
          "initialValue": {
            result: [],
            updated: false
          },
          "in": {
            "$cond": {
              "if": {
                $and: [
                  {
                    $eq: [
                      false,
                      "$$value.updated"
                    ]
                  },
                  {
                    $eq: [
                      "empty",
                      "$$this.userId"
                    ]
                  }
                ]
              },
              "then": {
                result: {
                  "$concatArrays": [
                    "$$value.result",
                    [
                      {
                        "name": "Wayne Wrestler",
                        "userId": ObjectId("636c1778f1d09191074f9690")
                      }
                    ]
                  ]
                },
                updated: true
              },
              "else": {
                result: {
                  "$concatArrays": [
                    "$$value.result",
                    [
                      "$$this"
                    ]
                  ]
                },
                updated: "$$value.updated"
              }
            }
          }
        }
      }
    }
  },
  {
    $set: {
      "weights.spotsAvailable": "$results.result",
      "results": "$$REMOVE"
    }
  },
  {
    $group: {
      _id: "$_id",
      "name": {
        $first: "$name"
      },
      "createdBy": {
        $first: "$createdBy"
      },
      "description": {
        $first: "$description"
      },
      "date": {
        $first: "$date"
      },
      "location": {
        $first: "$location"
      },
      "weights": {
        $push: "$weights"
      },
      "eventApplicants": {
        $first: "$eventApplicants"
      }
    }
  },
  {
    "$merge": {
      "into": "collection",
      "on": "_id"
    }
  }
])

Mongo Playground

CodePudding user response:

One option is to use update with pipeline:

Since this is a double nested array, it is easier to do it in two steps - internal and external

  1. First create the "external" item to replace in weights array and call it newItem. It is calculated using $reduce which allow us to manipulate the internal array while looping on it.
  2. Replace the relevant item on weights array with our newItem using $map with $cond
db.collection.update(
  {_id: ObjectId("6398c34ca67dbe3286452f23"), "weights.weight": 12},
  [
    {$set: {
      newItem: {$reduce: {
          input: {$getField: {
              input: {$first: {
                  $filter: {
                    input: "$weights",
                    as: "item",
                    cond: {$eq: ["$$item.weight", 12]}
                  }
              }},
              field: "spotsAvailable"
          }},
          initialValue: [],
          in: {$concatArrays: [
              "$$value",
              {$cond: [
                  {$and: [
                      {$eq: ["$$this.userId", "empty"]},
                      {$not: {$in: [ObjectId("636c1778f1d09191074f9690"), "$$value.userId"]}}
                  ]},
                  [{
                      name: "Wayne Wrestler",
                      userId: ObjectId("636c1778f1d09191074f9690")
                  }],
                  ["$$this"]
                ]}
            ]}
      }}
  }},
  {$set: {
      weights: {$map: {
          input: "$weights",
          in: {$cond: [
              {$eq: ["$$this.weight", 12]},
              {$mergeObjects: [
                  "$$this",
                  {spotsAvailable: "$newItem"}
              ]},
              "$$this"
          ]}
      }},
      newItem: "$$REMOVE"
  }}
])

See how it works on the playground example

  • Related