Update a specific array element if it fulfils at least one of several conditions

Question:

I am trying to understand (and fix) a code that is not mine, which uses PyMongo.

We want to look for the document with _id==_id that has, inside its list comments, a comment with id==comment_id or id==PyObjectId(comment_id).
To that comment we want to add an answer.
In code:

await db_collection.update_one(
        filter=query, update=update, array_filters=array_filters
)

where

query = {
    "_id": _id,
    "$or": [    
        {"comments": {"$elemMatch": {"id": comment_id}}},
        {"comments": {"$elemMatch": {"id": PyObjectId(comment_id)}}},
    ],
}

update = {
    "$set": {
        "comments.$[cmt].answer": jsonable_encoder(reply)
    }
}

array_filters = [{"cmt.id": comment_id}]

My problem is that array_filters only check if id==comment_id and not also if id == PyObjectId(comment_id), like the query does.
This way when I have a PyObjectId as id, no item is updated.

I guess I should modify array_filters with something like

array_filters = [{"cmt.id": comment_id}, {"cmt.id": PyObjectId(comment_id)}]

or

array_filters = [{"$or$": [{ "cmt.id": comment_id}, {"cmt.id": PyObjectId(comment_id)}]}

but sadly I can just test my code on production and I’m trying to understand how this actually works before breaking things.

Thank you all!

Asked By: chc

||

Answers:

Let’s see what’s going on here…

From the original question:

query = {
    "_id": _id,
    "$or": [    
        {"comments": {"$elemMatch": {"id": comment_id}}},
        {"comments": {"$elemMatch": {"id": PyObjectId(comment_id)}}},
    ],
}

update = {
    "$set": {
        "comments.$[cmt].answer": jsonable_encoder(reply)
    }
}

array_filters = [{"cmt.id": comment_id}]

The query is using "$elemMatch", but since there is only one condition on a single field, this could be simplified to:

query = {
    "_id": _id,
    "comments.id": {
        "$in": [comment_id, PyObjectId(comment_id)]
    },
}

If we assume (seems reasonable here, but we should be careful) that "id" is unique in the "comments" array, then "arrayFilters" isn’t necessary either. So update may be simplified to:

update = {
    "$set": {
        "comments.$.answer": jsonable_encoder(reply)
    }
}

Here $ uses the element "match" from query and replaces the first, and only the first, matching element in "comments" for the update. If more than one element of the "comments" array should be updated, this won’t work (see below use of "arrayFilters" if more than one element should be updated because "id" is not unique).

So, putting it all together with the assumptions we’ve made:

query = {
    "_id": _id,
    "comments.id": {
        "$in": [comment_id, PyObjectId(comment_id)]
    },
}

update = {
    "$set": {
        "comments.$.answer": jsonable_encoder(reply)
    }
}

# possibly some other code here from the original app ...

await db_collection.update_one(
        filter=query, update=update
)

See how it works with made-up documents on mongoplayground.net. Only one element is updated, even though there are duplicate "id" values.

With "arrayFilters"

If "id" is not unique within the "comments" array and multiple objects within the array should be updated, then "arrayFilters" can be used to do this.

query = {
    "_id": _id,
    "comments.id": {
        "$in": [comment_id, PyObjectId(comment_id)]
    },
}

update = {
    "$set": {
        "comments.$[cmt].answer": jsonable_encoder(reply)
    }
}

array_filters = [
    {
        "cmt.id": {
            "$in": [comment_id, PyObjectId(comment_id)]
        }
    }
]

# possibly some other code here from the original app ...

await db_collection.update_one(
        filter=query, update=update, array_filters=array_filters
)

Here, any "comments" array element that "matches" the "arrayFilter" will be updated. See how it works on mongoplayground.net. All elements that "match" the "arrayFilter" are updated.

Answered By: rickhg12hs
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.