I have searched SO and crawled the Mongoose / Mongo documentation but to no avail, therefore my question.
I would like to $inc a value in an object that lies within a nested array OR create $setOnInsert this object if it's not there yet.
The Mongo document I have looks as follows:
{
  "_id": "123",
  "members": [
    {
      "first": "johnny",
      "last": "knoxville",
      "score": 2
    },
    {
      "first": "johnny",
      "last": "cash",
      "score": 3
    },
    // ...and so on
  ],
}
Based on this example my use case is to:
count variable inside the array object if it exists (found based on first and last)score as 1 if it does not exist yetFrom this post I understood that I cannot $set a variable that I would like to $inc at the same time. Ok - that makes sense.
This post helped to understand the positional $ operator in order to find the nested object and increment it.
If I know that the document exists, I can simply do the update as follows:
myDoc = { first: 'johnny', last: 'cash' };
myCollection.findOneAndUpdate(
  {
    _id: '123',
    'members.first': 'johnny',
    'members.last': 'cash'
  },
  {
    $inc: { "members.$.score": 1 }
  }
);
But what if I would like to insert the member (with score: 1) if it doesn't exist yet?
My problem is that when I use upsert: true the positional operator throws an error since it may not be used with upsert (see the official documentation).
I have tried various combinations and would like to avoid 2 db accesses (read / write).
Is there a way to do this in ONE operation?
With MongoDB 4.2 and newer, the update method can now take a document or an aggregate pipeline where the following stages can be used:
$addFields and its alias $set
$project and its alias $unset
$replaceRoot and its alias $replaceWith.Armed with the above, your update operation with the aggregate pipeline will be to override the members field based on a condition i.e. the template follows:
var myDoc = { first: 'johnny', last: 'cash', score: 1 };
db.getCollection('myCollection').update(
    { "_id" : "123" },
    [
        "$set": {
            "members": {
                "$cond": {
                    "if": {}, // does the members array contain myDoc?
                    "then": {}, // map the members array and increment the score
                    "else": {} // add myDoc to the existing members array
                }
            }
        }
    ]
);
To get the expression for the first condition does the members array contain myDoc?, you need a way to $filter the array based on the condition that satisfies the first and last properties having the same values as myDoc respectively i.e.
{
    "$filter": {
        "input": "$members",
        "cond": {
            "$and": [
                { "$eq": ["$$this.first", myDoc.first] },
                { "$eq": ["$$this.last", myDoc.last] },
            ]
        }
    }
}
Check the first element of the resulting array with $arrayElemAt
{
    "$arrayElemAt": [
        {
            "$filter": {
                "input": "$members",
                "cond": {
                    "$and": [
                        { "$eq": ["$$this.first", myDoc.first] },
                        { "$eq": ["$$this.last", myDoc.last] },
                    ]
                }
            }
        },
        0
    ]
}
If there is no match then the above will be null and we can substitute the null with a value that can be used as the main condition with $ifNull:
{
    "$ifNull": [
        {
            "$arrayElemAt": [
                {
                    "$filter": {
                        "input": "$members",
                        "cond": {
                            "$and": [
                                { "$eq": ["$$this.first", myDoc.first] },
                                { "$eq": ["$$this.last", myDoc.last] },
                            ]
                        }
                    }
                },
                0
            ]
        },
        0
    ]   
}
The above becomes basis for our condition for the first IF statement using $ne to check for inequality:
{ "$ne": [
    {
        "$ifNull": [
            {
                "$arrayElemAt": [
                    {
                        "$filter": {
                            "input": "$members",
                            "cond": {
                                "$and": [
                                    { "$eq": ["$$this.first", myDoc.first] },
                                    { "$eq": ["$$this.last", myDoc.last] },
                                ]
                            }
                        }
                    },
                    0
                ]
            },
            0
        ]   
    },
    0
] }
If the above condition is true, then the $map expression becomes
{                        
    "$map": {
        "input": "$members",
        "in": {
            "$cond": [
                { "$eq": [{ "$ifNull": ["$$this.score", 0 ] }, 0] },
                { "$mergeObjects": ["$$this", { "score": 1 } ] },
                { "$mergeObjects": ["$$this", { "score": { "$sum": ["$$this.score", 1] } } ] }
            ]
        }
    }
}
else add the new document to the existing members array with $concatArrays
{
    "$concatArrays": [
        { "$ifNull": ["$members", []] },
        [ myDoc ]
    ]
}
Your final update operation becomes:
var myDoc = { first: 'johnny', last: 'cash', score: 1 };
db.getCollection("myCollection").update(
    { "_id" : "123" },
    [
        { "$set": {
            "members": {
                "$cond": {
                    "if": {
                        "$ne": [
                            {
                                "$ifNull": [
                                    {
                                        "$arrayElemAt": [
                                            {
                                                "$filter": {
                                                    "input": "$members",
                                                    "cond": {
                                                        "$and": [
                                                            { "$eq": ["$$this.first", myDoc.first] },
                                                            { "$eq": ["$$this.last", myDoc.last] },
                                                        ]
                                                    }
                                                }
                                            },
                                            0
                                        ]
                                    },
                                    0
                                ]   
                            },
                            0
                        ]
                    },
                    "then": {
                        "$map": {
                            "input": "$members",
                            "in": {
                                "$cond": [
                                    { "$eq": [{ "$ifNull": ["$$this.score", 0 ] }, 0] },
                                    { "$mergeObjects": ["$$this", { "score": 1 } ] },
                                    { "$mergeObjects": ["$$this", { "score": { "$sum": ["$$this.score", 1] } } ] }
                                ]
                            }
                        }
                    },
                    "else": {
                        "$concatArrays": [
                            { "$ifNull": ["$members", []] },
                            [ myDoc ]
                        ]
                    }
                }
            }
        } }
    ],
    { "upsert": true } 
);
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With