MongoDB - 更新或插入数组中的对象

57

我有以下收藏

{
    "_id" : ObjectId("57315ba4846dd82425ca2408"),
    "myarray" : [ 
        {
            userId : ObjectId("570ca5e48dbe673802c2d035"),
            point : 5
        },
        {
            userId : ObjectId("613ca5e48dbe673802c2d521"),
            point : 2
        },        
     ]
}

这是我的问题

如果userId不存在,我想将其推入myarray,应将其追加到myarray。如果userId存在,则应更新为指向的内容。

我找到了这个

db.collection.update({
    _id : ObjectId("57315ba4846dd82425ca2408"),
    "myarray.userId" :  ObjectId("570ca5e48dbe673802c2d035")
}, {
    $set: { "myarray.$.point": 10 }
})

但是如果 userId 不存在,什么也不会发生。

db.collection.update({
    _id : ObjectId("57315ba4846dd82425ca2408")
}, {
  $push: {
      "myarray": {
          userId: ObjectId("570ca5e48dbe673802c2d035"),
          point: 10
      }
  }
})

但是如果 userId 对象已经存在,则会再次进行推送。

MongoDB中最好的方法是什么?


为了参考,我在下面的链接中添加了我的冗长解决方案,涵盖了相同的情况,即“如何将新对象添加到对象数组中,除非存在特定对象值(例如userId),在这种情况下更新该对象”:https://stackoverflow.com/a/52671119 - user1063287
10个回答

34

试一下这个

db.collection.update(
    { _id : ObjectId("57315ba4846dd82425ca2408")},
    { $pull: {"myarray.userId": ObjectId("570ca5e48dbe673802c2d035")}}
)
db.collection.update(
    { _id : ObjectId("57315ba4846dd82425ca2408")},
    { $push: {"myarray": {
        userId:ObjectId("570ca5e48dbe673802c2d035"),
        point: 10
    }}
)

说明: 在第一个语句中,$pull从文档的数组中删除具有userId = ObjectId("570ca5e48dbe673802c2d035") 的元素,该文档的_id = ObjectId("57315ba4846dd82425ca2408")
在第二个语句中,$push将对象{ userId:ObjectId("570ca5e48dbe673802c2d035"), point: 10 }插入到同一数组中。

4
希望您能明确说明您的代码做了什么,这样对未来的读者会很有帮助。 - Webwoman
好的,那很棒,但在MongoDb中,id应该自动插入,而你却手动添加了它。 - Saddam Mohsen
1
两种不同的呼叫方式,是一个很好的想法但是价格昂贵。 - Gabriel Lupu

33

根据Flying Fisher的被接受的答案,现有记录首先将被删除,然后再次推送。

更安全的方法(常识)是首先尝试更新记录,如果未找到匹配,则插入它,如下所示:

// first try to overwrite existing value
var result = db.collection.update(
   {
       _id : ObjectId("57315ba4846dd82425ca2408"),
       "myarray.userId": ObjectId("570ca5e48dbe673802c2d035")
   },
   {
       $set: {"myarray.$.point": {point: 10}}
   }
);
// you probably need to modify the following if-statement to some async callback
// checking depending on your server-side code and mongodb-driver
if(!result.nMatched)
{
    // record not found, so create a new entry
    // this can be done using $addToSet:
    db.collection.update(
        {
            _id: ObjectId("57315ba4846dd82425ca2408")
        },
        {
            $addToSet: {
                myarray: {
                    userId: ObjectId("570ca5e48dbe673802c2d035"),
                    point: 10
                }
            }
        }
    );
    // OR (the equivalent) using $push:
    db.collection.update(
        {
            _id: ObjectId("57315ba4846dd82425ca2408"),
            "myarray.userId": {$ne: ObjectId("570ca5e48dbe673802c2d035"}}
        },
        {
            $push: {
                myarray: {
                    userId: ObjectId("570ca5e48dbe673802c2d035"),
                    point: 10
                }
            }
        }
    );
}

如果在大多数情况下记录已经存在,则这也应该(常识,未经测试)提高性能,因为只有第一次查询将被执行。


如果两个并发请求同时进入if语句,会发生什么?只有其中一个$addToSet会生效,是吗? - Fabio Formosa
@FabioFormosa 没错,但你说得对,这仍然是不安全的。如果同时执行了$addToSet和更新了points怎么办... 这段代码肯定应该在事务中进行。 - Yeti

18

MongoDB v4.2开始,有一个名为使用聚合管道更新文档的选项,

  • 检查条件$cond是否在myarray.userId中存在userId
  • 如果是,则使用$map迭代循环myarray数组,并检查条件是否匹配userId,然后使用$mergeObjects与新文档进行合并
  • 如果不是,则使用$concatArrays将新对象和myarray连接起来
let _id = ObjectId("57315ba4846dd82425ca2408");
let updateDoc = {
  userId: ObjectId("570ca5e48dbe673802c2d035"),
  point: 10
};

db.collection.update(
  { _id: _id },
  [{
    $set: {
      myarray: {
        $cond: [
          { $in: [updateDoc.userId, "$myarray.userId"] },
          {
            $map: {
              input: "$myarray",
              in: {
                $mergeObjects: [
                  "$$this",
                  {
                    $cond: [
                      { $eq: ["$$this.userId", updateDoc.userId] },
                      updateDoc,
                      {}
                    ]
                  }
                ]
              }
            }
          },
          { $concatArrays: ["$myarray", [updateDoc]] }
        ]
      }
    }
  }]
)

游乐场


1
这是解决问题的好方法。我试图通过首先设置要更新的元素的索引并使用它来避免多次遍历数组来“改进”它。但我无法弄清如何使用该索引仅更新正确的元素,就像这里一样。所以我会采用你的解决方案! - gdollardollar

3

很遗憾,嵌套数组不支持"upsert"(更新或插入)操作。这些运算符根本不存在,所以无法在单个语句中实现。因此,您必须执行两个更新操作才能实现您的目标。此外,在这两个更新操作中应用的顺序对于获得所需结果非常重要。


3

我还没有找到任何基于一个原子查询的解决方案。相反,有三种基于两个查询序列的方法:

  1. always $pull (to remove the item from array), then $push (to add the updated item to array)

    db.collection.update(
                   { _id : ObjectId("57315ba4846dd82425ca2408")},
                   { $pull: {"myarray.userId": ObjectId("570ca5e48dbe673802c2d035")}}
    )
    
    db.collection.update(
                   { _id : ObjectId("57315ba4846dd82425ca2408")},
                   {
                     $push: {
                              "myarray": {
                                          userId:ObjectId("570ca5e48dbe673802c2d035"),
                                          point: 10
                                         }
                             }
                    }
    )
    
  2. try to $set (to update the item in array if exists), then get the result and check if the updating operation successed or if a $push needs (to insert the item)

    var result = db.collection.update(
        {
           _id : ObjectId("57315ba4846dd82425ca2408"),
          "myarray.userId": ObjectId("570ca5e48dbe673802c2d035")
        },
        {
           $set: {"myarray.$.point": {point: 10}}
        }
     );
    
    if(!result.nMatched){
           db.collection.update({_id: ObjectId("57315ba4846dd82425ca2408")},
                                {
                                  $addToSet: {
                                               myarray: {
                                                  userId: ObjectId("570ca5e48dbe673802c2d035"),
                                                  point: 10
                                              }
                                }
           );
    
  3. always $addToSet (to add the item if not exists), then always $set to update the item in array

       db.collection.update({_id: ObjectId("57315ba4846dd82425ca2408")},
                             myarray: { $not: { $elemMatch: {userId: ObjectId("570ca5e48dbe673802c2d035")} } } },
                            { 
                               $addToSet : {
                                             myarray: {
                                                        userId: ObjectId("570ca5e48dbe673802c2d035"),
                                                        point: 10
                                                       }
                                            }
                             },
                            { multi: false, upsert: false});
    
       db.collection.update({
                              _id: ObjectId("57315ba4846dd82425ca2408"),
                               "myArray.userId": ObjectId("570ca5e48dbe673802c2d035")
                            },
                            { $set : { myArray.$.point: 10 } },
                            { multi: false, upsert: false});
    

第一种和第二种方法是不安全的,因此必须建立事务以避免两个并发请求可能会push相同的项生成重复。

第三种方式更加安全。如果该项不存在,则$addToSet仅添加该项,否则不会发生任何事情。在两个并发请求的情况下,只有其中一个将缺少的项添加到数组中。


第三种方法如何安全?如果并发请求修改了数组元素,使得“points”不再是10,则第一个调用将为相同的“userId”添加第二个元素到数组中。 - fluidsonic
如果存在一个相同的“userId”但具有不同“point”值的数组元素,则会将第二个“userId”的元素添加到数组中。 “$addToSet”方法仅在“point”始终是唯一可能的“10”值时才有效,这种情况是不太可能出现的。 - fluidsonic
$addToSet 只比较整个数组元素。{ userId: 1, point: 2 }{ userId: 1, point: 4 }不同的元素,因此 $addToSet 将添加这两个条目。$addToSet 无法知道 userId 具有某些特殊的索引意义。 - fluidsonic
1
啊,现在我明白你的意思了。我错过了$elemMatch因为我没有滚动到足够远的位置。在这种情况下,你是正确的 - 它应该可以工作。你甚至可以使用$push代替$addToSet,因为你已经通过$elemMatch过滤器基本上涵盖了集合行为。 - fluidsonic
1
我刚刚重新编辑了那行代码,以避免水平滚动条引起的误解。这可能对其他人有用。非常感谢您的反馈。 - Fabio Formosa
显示剩余3条评论

1

使用聚合管道的可能解决方案:

db.collection.update(
      { _id },
      [
        {
          $set: {
            myarray: { $filter: {
              input: '$myarray',
              as: 'myarray',
              cond: { $ne: ['$$myarray.userId', ObjectId('570ca5e48dbe673802c2d035')] },
            } },
          },
        },
        {
          $set: {
            myarray: {
              $concatArrays: [
                '$myarray',
                [{ userId: ObjectId('570ca5e48dbe673802c2d035'), point: 10 },
                ],
              ],
            },
          },
        },
      ],
    );

我们使用了2个步骤:
  1. 筛选myarray(即如果存在userId则删除元素)。
  2. 将筛选后的myarray与新元素合并。

0

2
很不幸,upsert选项并不能真正解决这个问题,因为它需要数组的精确匹配(即查询中所有数组元素都必须明确定义)。 如果没有找到匹配项,则upsert会创建一个全新的文档,但它不会将新元素“upsert”到一个数组中。 - danj1974

0

当您想要更新或插入数组中的值时,请尝试它

数据库中的对象

key:name,
key1:name1,
arr:[
    {
        val:1,
        val2:1
    }
]

查询

var query = {
    $inc:{
        "arr.0.val": 2,
        "arr.0.val2": 2
    }
}
.updateOne( { "key": name }, query, { upsert: true }

key:name,
key1:name1,
arr:[
    {
        val:3,
        val2:3
    }
]

-1

数组更新和创建不要混在一个查询中,如果您非常关注原子性,则有以下解决方案:

将模式规范化为,

{
    "_id" : ObjectId("57315ba4846dd82425ca2408"),
    userId : ObjectId("570ca5e48dbe673802c2d035"),
    point : 5
}

创建或更新此模式文档可以在一个查询中完成。 - Gagandeep Kalra

-2
您可以使用 mongosh CLI 中我当前使用的.forEach/.updateOne方法 的变体来执行类似的操作。在.forEach中,您可能能够设置所有您提到的if/then条件。 .forEach/.updateOne示例:
let medications = db.medications.aggregate([
  {$match: {patient_id: {$exists: true}}}
]).toArray();

medications.forEach(med => {
  try {
    db.patients.updateOne({patient_id: med.patient_id}, 
      {$push: {medications: med}}
    )
  } catch {
    console.log("Didn't find match for patient_id.  Could not add this med to a patient.")
  }  
})

这可能不是最符合“MongoDB方式”的做法,但它绝对有效,并且让你在.forEach内部使用JavaScript自由地进行操作。


这并没有回答问题,而且非常不符合惯用语(为什么在第一行中使用聚合来进行简单查询?)。如果您要使用聚合,最好对整个集合进行聚合,在线执行更新,然后将输出与原始集合进行$merge。但正如其他人所提到的,最好的解决方案可能是来自MongoDB 4.2+的update + aggregation pipeline语法。此外,如果您以这种方式执行操作,则最好使用.forEach而不是.toArray来进行初始查询。 - ephemer
感谢提供的信息。在MongoDB操作方面,我确实有很多需要学习的地方,现在仍然如此。 - amota

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接