【问题标题】:MongoDB $inc and $setOnInsert for nested array用于嵌套数组的 MongoDB $inc 和 $setOnInsert
【发布时间】:2025-11-30 23:55:01
【问题描述】:

我已经搜索了 SO 并抓取了 Mongoose / Mongo 文档,但无济于事,因此我的问题是。

我想$inc 一个位于嵌套数组中的对象中的值,或者创建$setOnInsert 这个对象,如果它还不存在的话。

我拥有的 Mongo 文档如下所示:

{
  "_id": "123",
  "members": [
    {
      "first": "johnny",
      "last": "knoxville",
      "score": 2
    },
    {
      "first": "johnny",
      "last": "cash",
      "score": 3
    },
    // ...and so on
  ],
}

基于这个例子,我的用例是:

  • 如果存在数组对象内的count 变量,则增加它(基于firstlast 找到)
  • 如果对象尚不存在,则将score 设为 1 的对象添加到集合中

this post 我了解到我不能同时$set 一个我想$inc 的变量。好的 - 这是有道理的。

This post 有助于理解位置 $ 运算符,以便找到嵌套对象并递增它。

如果我知道该文档存在,我可以简单地进行如下更新:

myDoc = { first: 'johnny', last: 'cash' };

myCollection.findOneAndUpdate(
  {
    _id: '123',
    'members.first': 'johnny',
    'members.last': 'cash'
  },
  {
    $inc: { "members.$.score": 1 }
  }
);

但是如果我想插入成员(带有score: 1),如果它还不存在呢?

我的问题是,当我使用 upsert: true 时,位置运算符会引发错误,因为它可能无法与 upsert 一起使用(请参阅official documentation)。

我尝试了各种组合,并希望避免 2 db 访问(读/写)。

有没有办法在 ONE 操作中做到这一点?

【问题讨论】:

    标签: javascript mongodb mongoose


    【解决方案1】:

    对于 MongoDB 4.2 及更高版本,更新方法现在可以获取文档或an aggregate pipeline,其中可以使用以下阶段:

    1. $addFields 及其别名 $set
    2. $project 及其别名 $unset
    3. $replaceRoot 及其别名 $replaceWith

    有了上述内容,您对聚合管道的更新操作将根据条件覆盖成员字段,即模板如下:

    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
                    }
                }
            }
        ]
    );
    

    要获得第一个条件的表达式成员数组是否包含 myDoc?,您需要一种方法来 $filter 基于满足第一个和最后一个属性相同的条件的数组值分别为myDoc,即

    {
        "$filter": {
            "input": "$members",
            "cond": {
                "$and": [
                    { "$eq": ["$$this.first", myDoc.first] },
                    { "$eq": ["$$this.last", myDoc.last] },
                ]
            }
        }
    }
    

    $arrayElemAt检查结果数组的第一个元素

    {
        "$arrayElemAt": [
            {
                "$filter": {
                    "input": "$members",
                    "cond": {
                        "$and": [
                            { "$eq": ["$$this.first", myDoc.first] },
                            { "$eq": ["$$this.last", myDoc.last] },
                        ]
                    }
                }
            },
            0
        ]
    }
    

    如果不匹配,则上述内容将为 null,我们可以将 null 替换为可用作 $ifNull 的主要条件的值:

    {
        "$ifNull": [
            {
                "$arrayElemAt": [
                    {
                        "$filter": {
                            "input": "$members",
                            "cond": {
                                "$and": [
                                    { "$eq": ["$$this.first", myDoc.first] },
                                    { "$eq": ["$$this.last", myDoc.last] },
                                ]
                            }
                        }
                    },
                    0
                ]
            },
            0
        ]   
    }
    

    以上内容成为我们使用$ne检查不等式的第一个IF语句的条件的基础:

    { "$ne": [
        {
            "$ifNull": [
                {
                    "$arrayElemAt": [
                        {
                            "$filter": {
                                "input": "$members",
                                "cond": {
                                    "$and": [
                                        { "$eq": ["$$this.first", myDoc.first] },
                                        { "$eq": ["$$this.last", myDoc.last] },
                                    ]
                                }
                            }
                        },
                        0
                    ]
                },
                0
            ]   
        },
        0
    ] }
    

    如果上述条件为真,则$map表达式变为

    {                        
        "$map": {
            "input": "$members",
            "in": {
                "$cond": [
                    { "$eq": [{ "$ifNull": ["$$this.score", 0 ] }, 0] },
                    { "$mergeObjects": ["$$this", { "score": 1 } ] },
                    { "$mergeObjects": ["$$this", { "score": { "$sum": ["$$this.score", 1] } } ] }
                ]
            }
    
        }
    }
    

    使用$concatArrays 将新文档添加到现有成员数组中

    {
        "$concatArrays": [
            { "$ifNull": ["$members", []] },
            [ myDoc ]
        ]
    }
    

    您的最终更新操作变为:

    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 } 
    );
    

    【讨论】:

    • 太棒了!谢谢@chridam