Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB insert document "or" increment field if exists in array

Tags:

mongodb

What I try to do is fairly simple, I have an array inside a document ;

"tags": [ 
    {
        "t" : "architecture",
        "n" : 12
    }, 
    {
        "t" : "contemporary",
        "n" : 2
    }, 
    {
        "t" : "creative",
        "n" : 1
    }, 
    {
        "t" : "concrete",
        "n" : 3
    }
]

I want to push an array of items to array like

["architecture","blabladontexist"]

If item exists, I want to increment object's n value (in this case its architecture),

and if don't, add it as a new Item (with value of n=0) { "t": "blabladontexist", "n":0}

I have tried $addToSet, $set, $inc, $upsert: true with so many combinations and couldn't do it.

How can we do this in MongoDB?

like image 452
Sadettin Bilal Savaş Avatar asked Aug 08 '16 16:08

Sadettin Bilal Savaş


People also ask

How do I increment a field in MongoDB?

The $inc operator increments a field by a specified value and has the following form: { $inc: { <field1>: <amount1>, <field2>: <amount2>, ... } } To specify a <field> in an embedded document or in an array, use dot notation.

Does MongoDB update insert if not exists?

You can use upsert i.e. whenever you insert a value and it already exist then update would be performed. If the value does not already exist then it would get inserted.

Is there an Upsert option in the MongoDB insert command?

Here in MongoDB, the upsert option is a Boolean value. Suppose the value is true and the documents match the specified query filter. In that case, the applied update operation will update the documents. If the value is true and no documents match the condition, this option inserts a new document into the collection.


1 Answers

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:

  1. $addFields and its alias $set
  2. $project and its alias $unset
  3. $replaceRoot and its alias $replaceWith.

Armed with the above, your update operation with the aggregate pipeline will be to override the tags field by concatenating a filtered tags array and a mapped array of the input list with some data lookup in the map:

To start with, the aggregate expression that filters the tags array uses the $filter and it follows:

const myTags = ["architecture", "blabladontexist"];

{ 
    "$filter": { 
        "input": "$tags",
        "cond": { 
            "$not": [
                { "$in": ["$$this.t", myTags] } 
            ] 
        }
    } 
}

which produces the filtered array of documents

[  
    { "t" : "contemporary", "n" : 2 }, 
    { "t" : "creative", "n" : 1 }, 
    { "t" : "concrete", "n" : 3 }
]

Now the second part will be to derive the other array that will be concatenated to the above. This array requires a $map over the myTags input array as

{ 
    "$map": { 
        "input": myTags,
        "in": {
            "$cond": {
                "if": { "$in": ["$$this", "$tags.t"] },
                "then": { 
                    "t": "$$this", 
                    "n": { 
                        "$sum": [
                            { 
                                "$arrayElemAt": [
                                    "$tags.n", 
                                    { "$indexOfArray": [ "$tags.t", "$$this" ] } 
                                ] 
                            },
                            1
                        ]
                    } 
                },
                "else": { "t": "$$this", "n": 0 }
            }
        }
    } 
}

The above $map essentially loops over the input array and checks with each element whether it's in the tags array comparing the t property, if it exists then the value of the n field of the subdocument becomes its current n value expressed with

{ 
    "$arrayElemAt": [
        "$tags.n", 
        { "$indexOfArray": [ "$tags.t", "$$this" ] } 
    ] 
}

else add the default document with an n value of 0.

Overall, your update operation will be as follows

Your final update operation becomes:

const myTags = ["architecture", "blabladontexist"];

db.getCollection('coll').update(
    { "_id": "1234" },
    [
        { "$set": {
            "tags": {
                "$concatArrays": [
                    { "$filter": { 
                        "input": "$tags",
                        "cond": { "$not": [ { "$in": ["$$this.t", myTags] } ] }
                    } },
                    { "$map": { 
                        "input": myTags,
                        "in": {
                            "$cond": [
                                { "$in": ["$$this", "$tags.t"] },
                                { "t": "$$this", "n": { 
                                    "$sum": [
                                        { "$arrayElemAt": [
                                            "$tags.n", 
                                            { "$indexOfArray": [ "$tags.t", "$$this" ] } 
                                        ] },
                                        1
                                    ]
                                } },
                                { "t": "$$this", "n": 0 }
                            ]
                        }
                    } }
                ]
            }
        } }
    ],
    { "upsert": true }
);
like image 100
chridam Avatar answered Oct 27 '22 09:10

chridam