databasemongodbmongoosepositional-operator

positional operator ($) not working correctly


I am trying to find (and update) a particular object from items array of the cart collection. A cart documnet looks something like this

{
    _id: ObjectId("661fb9ddc2b236ae8703ba92"),
    display_id: '38bd6126-02a1-47b9-b567-00aaf2eb076f',
    user_id: ObjectId("661137f22a31832ceb92ddbc"),
    items: [
      {
        product_id: ObjectId("6622376051d5886f39ac7365"),
        size: ObjectId("660d444d0d7716ee24129675"),
        qty: 3
      },
      {
        product_id: ObjectId("662214f8cc5306f999369e99"),
        size: ObjectId("660d444d0d7716ee24129675"),
        qty: 2
      },
      {
        product_id: ObjectId("662214f8cc5306f999369e99"),
        size: ObjectId("660d444d0d7716ee24129674"),
        qty: 2
      }
    ],
    __v: 4
  }

The query I am trying to run is

db.carts.find({user_id: new ObjectId("661137f22a31832ceb92ddbc"), "items.product_id": new ObjectId("662214f8cc5306f999369e99"), "items.size":new ObjectId("660d444d0d7716ee24129675")}, {"items.$":1})

or in mongoose

const doc = await Cart.findOne({
                user_id: user_id,
                "items.product_id": product_id,
                "items.size": oldSize
            },{"items.$":1})

Ideally it should return this object :

{
 product_id: ObjectId("662214f8cc5306f999369e99"),
 size: ObjectId("660d444d0d7716ee24129675"),
 qty: 2
}

but it returns this one :

{
 product_id: ObjectId("6622376051d5886f39ac7365"),
 size: ObjectId("660d444d0d7716ee24129675"),
 qty: 3
},

Please tell what am I doing wrong.


Solution

  • It could be confusing about the query when working with sub-documents. For your current query, it is actually querying 3 things in an AND manner:

    1. any document with user_id: 661137f22a31832ceb92ddbc
    2. any document with either 1 subdocument items that have product_id: 662214f8cc5306f999369e99
    3. any document with either 1 subdocument items that have size: 660d444d0d7716ee24129675

    Note that for #2 and #3, they don't need to be the same subdocument. In other words, a document with 2 separate subdocuments fulfilling only #2 or #3 can also be returned.

    For your case, your document has items that fulfill both #2 and #3, but your projection clause (i.e. {"items.$":1}) is just asking to return the first element in the array.

    So, what can you do to make sure #2 and #3 is matching the same subdocument? You can use $elemMatch. By this way, you can also use the positional operator $ in the projection clause.

    db.collection.find({
      "user_id": ObjectId("661137f22a31832ceb92ddbc"),
      "items": {
        "$elemMatch": {
          "product_id": ObjectId("662214f8cc5306f999369e99"),
          "size": ObjectId("660d444d0d7716ee24129675")
        }
      }
    },
    {
      "items.$": 1
    })
    

    Mongo Playground


    In fact, if you find yourself working at the items level most often, you are suggested to "flatten" the items entry into individual documents like below. You can potentially gain benefits like simpler queries and performance boosts from indexing.

    {
        display_id: "38bd6126-02a1-47b9-b567-00aaf2eb076f",
        user_id: ObjectId("661137f22a31832ceb92ddbc"),
        product_id: ObjectId("662214f8cc5306f999369e99"),
        size: ObjectId("660d444d0d7716ee24129675"),
        qty: 2
    }
    

    Mongo Playground