我该如何从MongoDB数组中删除重复的对象?

10

我的数据长这样:

{

    "foo_list": [
      {
        "id": "98aa4987-d812-4aba-ac20-92d1079f87b2",
        "name": "Foo 1",
        "slug": "foo-1"
      },
      {
        "id": "98aa4987-d812-4aba-ac20-92d1079f87b2",
        "name": "Foo 1",
        "slug": "foo-1"
      },
      {
        "id": "157569ec-abab-4bfb-b732-55e9c8f4a57d",
        "name": "Foo 3",
        "slug": "foo-3"
      }
    ]
}

foo_list是名为Bar的模型中的一个字段。请注意,数组中的第一个和第二个对象完全重复。

除了明显的解决方案切换到PostgresSQL之外,我可以运行什么MongoDB查询来从foo_list中删除重复条目?

类似的答案并不完全适用:

这些问题回答了如果数组中有裸字符串的情况下的问题。然而在我的情况下,数组中填充的是对象。

我希望清楚地表达,我不想查询数据库;我希望将重复项永久从数据库中删除。

1个回答

17

纯粹从聚合框架的角度来看,有几种方法可以实现这个。

您可以在现代版本中直接应用$setUnion

 db.collection.aggregate([
     { "$project": { 
         "foo_list": { "$setUnion": [ "$foo_list", "$foo_list" ] }
     }}
 ])

或者更传统地使用$unwind$addToSet

db.collection.aggregate([
    { "$unwind": "$foo_list" },
    { "$group": {
        "_id": "$_id",
        "foo_list": { "$addToSet": "$foo_list" }
    }}
])

如果你只对重复项感兴趣,那么可以通过一般分组来实现:

db.collection.aggregate([
    { "$unwind": "$foo_list" },
    { "$group": {
        "_id": {
            "_id": "$_id",
            "foo_list": "$foo_list"
        },
        "count": { "$sum": 1 }
    }},
    { "$match": { "count": { "$ne": 1 } } },
    { "$group": {
        "_id": "$_id._id",
        "foo_list": { "$push": "$_id.foo_list" }
    }}
])    

如果你确实想用另一个更新语句从数据中“删除”重复项,那么最后一种形式可能对你有用,因为它可以识别出重复的元素。

因此,在最后一种形式中,从样本数据返回的结果标识了重复项:

{
    "_id" : ObjectId("53f5f7314ffa9b02cf01c076"),
    "foo_list" : [
            {
                    "id" : "98aa4987-d812-4aba-ac20-92d1079f87b2",
                    "name" : "Foo 1",
                    "slug" : "foo-1"
            }
    ]
}

当从包含重复条目的数组中返回每个文档的集合结果时,将返回哪些条目是重复的。这是您需要更新的信息,您需要循环结果以指定来自结果的更新信息,以删除重复项。

实际上,每个文档需要使用两个更新语句来完成此操作,因为简单的$pull操作会移除“两个”项目,而这不是您想要的:

var cursor = db.collection.aggregate([
    { "$unwind": "$foo_list" },
    { "$group": {
        "_id": {
            "_id": "$_id",
            "foo_list": "$foo_list"
        },
        "count": { "$sum": 1 }
    }},
    { "$match": { "count": { "$ne": 1 } } },
    { "$group": {
        "_id": "$_id._id",
        "foo_list": { "$push": "$_id.foo_list" }
    }}
])    

var batch = db.collection.initializeOrderedBulkOp();
var count = 0;

cursor.forEach(function(doc) {
    doc.foo_list.forEach(function(dup) {
        batch.find({ "_id": doc._id, "foo_list": { "$elemMatch": dup } }).updateOne({
            "$unset": { "foo_list.$": "" }
        });
        batch.find({ "_id": doc._id }).updateOne({ 
            "$pull": { "foo_list": null }
        });
    });
    
    count++;
    if ( count % 500 == 0 ) {
        batch.execute();
        batch = db.collection.initializeOrderedBulkOp();
    }
});

if ( count % 500 != 0 ) {
    batch.execute();
}

这是现代 MongoDB 2.6 及以上版本处理方式:使用聚合的游标结果和批量操作进行更新。但原则仍然相同:

  1. 识别文档中的重复项

  2. 循环处理结果,对受影响的文档进行更新

  3. 使用$unset位置占位符 $ 设置“第一个”匹配的数组元素为null

  4. 使用$pull 来移除数组中的null条目

因此,经过上述操作处理后,样本现在看起来像这样:

{
    "_id" : ObjectId("53f5f7314ffa9b02cf01c076"),
    "foo_list" : [
            {
                    "id" : "98aa4987-d812-4aba-ac20-92d1079f87b2",
                    "name" : "Foo 1",
                    "slug" : "foo-1"
            },
            {
                    "id" : "157569ec-abab-4bfb-b732-55e9c8f4a57d",
                    "name" : "Foo 3",
                    "slug" : "foo-3"
            }
    ]
}

使用“duplicated”项目仍然完整的方式删除重复项。这是您从集合中识别和删除重复数据的处理方式。


第二段代码清单与我给出的两个不起作用的答案的例子完全相同,因为这是一个对象数组,而不是一个字符串数组。 - andrewrk
1
@andrewrk MongoDB并不关心它们。它只是将它们视为“事物”。当然,所示的代码经过测试并且可以正常工作。除非我要离开某个地方(但这里不是这种情况),否则我总是在提交响应之前进行测试。 - Neil Lunn
@andrewrk 如果您认为这不起作用或在其他地方尝试过并失败,那么您呈现的对象实际上不是真正的副本。如果只有一个属性(例如“id”)是“重复”的,则数据不是真正的“集合”,您必须以不同的方式处理“去重”。但这不是您提出问题的方式。 - Neil Lunn
Neil的所有答案都是我认为寻找正确重复项的好方法,我相信他已经测试过了。它们都不会改变数据库中实际文档的内容-这就是为什么他提到使用最后一个管道来构建更新文档以删除重复项。没有一步操作可以从文档中删除重复项,但Neil的答案非常有帮助,可以作为删除重复项过程的一步。 - wdberkeley
我从来没有让这个工作起来。相反,我改变了我的代码,小心地避免创建重复项。 - andrewrk
显示剩余7条评论

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