11

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?

4
  • Have you looked at the aggregation pipeline at all? Commented Aug 8, 2016 at 18:23
  • 1
    I guess aggregation it`s not solution for the problem, cause it just select data from collection. @Sadettin Bilal Savaş need insert data. It has reason only if structure of collection will changed. Commented Aug 8, 2016 at 18:30
  • Would you consider changing the structure of your data as a solution? For example, you can have references to Tag entity IDs in your tags array. Commented Aug 8, 2016 at 23:08
  • hey josh, ofc its open for a change if theres no other way. I'm just trying to figure out if my question is possible with a single query, because upsert like actions are very common in MongoDB, and I couldn't find a way to do this simple update, what do you mean by tag entity IDs, and how would it help? thanks! Commented Aug 9, 2016 at 5:27

2 Answers 2

2

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 }
);
Sign up to request clarification or add additional context in comments.

3 Comments

This is totally incorrect!! How can you have concatarrays operator in a $set operation in an UPDATE transaction?? Concatarrays is only valid in aggregation pipeline.
He has a point, because you obviously never ran your final result yourself. It is correct that you can use an aggregation pipeline. BUT you need to wrap the second argument in square brackets [], otherwise it will not know you intend to use an aggregation pipeline.
Additionally, if you run this against a database, even with brackets, the final result will be { "_id" : "1234", "tags" : null } which is not the expected output.
0

I don't believe this is possible to do in a single command.

MongoDB doesn't allow a $set (or $setOnInsert) and $inc to affect the same field in a single command.

You'll have to do one update command to attempt to $inc the field, and if that doesn't change any documents (n = 0), do the update to $set the field to it's default value.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.