在MongoDB中对集合进行递归搜索

18
我是一名有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

我在MongoDB中有一个树形结构的文档列表,其中使用了带父引用的模型树结构模式。我想要一个单一的聚合查询,以给定“name”属性返回祖先列表(直到根)。

结构:

{
  '_id': '1',
  'name': 'A',
  'parent': '',
},
{
  '_id': '2',
  'name': 'B',
  'parent': 'A',
},
{
  '_id': '3',
  'name': 'C',
  'parent': 'B',
},
{
  '_id': '4',
  'name': 'D',
  'parent': 'C',
}

聚合结果:(给定,名称 = 'D')
{
  '_id': '4',
  'name': 'D',
  'ancestors': [{name:'C'}, {name:'B'}, {name:'A'}]
}

注意: 我现在不能更改文档结构,这会引起许多问题。我看到了很多解决方案建议使用带有祖先数组的模型树结构。但是我现在无法使用它。有没有办法使用上述模式使用单个聚合查询来实现呢?谢谢


为什么 _id 是字符串? - styvane
@Styvane 这只是一个示例。实际文档将会拥有ObjectId。 - RaR
@RaR Styvane的回答有什么问题吗?是什么促使你开启了悬赏? - JohnnyHK
@JohnnyHK Styvane的答案适用于MongoDB v3.4及以上版本,我正在寻找一种适用于MongoDB v2.6及更高版本的方法。 - RaR
2个回答

26

从MongoDB 3.4开始,我们可以使用聚合框架完成此操作。

管道中的第一个和最重要的阶段是$graphLookup阶段。 $graphLookup允许我们递归匹配"parent"和"name"字段。结果,我们获得了每个"name"的祖先。

管道中的下一个阶段是$match阶段,我们只需选择我们感兴趣的"name"。

最后一个阶段是$addFields$project阶段,我们使用$map数组运算符对"ancestors"数组应用表达式。

当然,使用$reverseArray运算符,我们可以反转数组以获得预期结果。
db.collection.aggregate(
    [ 
        { "$graphLookup": { 
            "from": "collection", 
            "startWith": "$parent", 
            "connectFromField": "parent", 
            "connectToField": "name", 
            "as": "ancestors"
        }}, 
        { "$match": { "name": "D" } }, 
        { "$addFields": { 
            "ancestors": { 
                "$reverseArray": { 
                    "$map": { 
                        "input": "$ancestors", 
                        "as": "t", 
                        "in": { "name": "$$t.name" }
                    } 
                } 
            }
        }}
    ]
)

嗨,如果给定name = 'A'而不是name = 'D',我需要如何更改它?我需要所有祖先为A的文档。 - user3629892
@user3629892,我不确定我理解你的问题。你是说你有一个'A'而不是'D'吗?如果是这样,只需将'D'替换为'A',或者更好的方法是将名称分配给一个变量,并在管道中使用该变量。 - styvane
1
我的意思是我有一些条目,每个条目都引用它们的父级。给定一个叶节点(没有子节点),我想找到它的父节点、父节点的父节点等等......作为输入,我不会得到父节点,而是叶节点,在 OP 的示例中,它将是 D 而不是 A。我会尝试你的建议。 - user3629892
嗨,我有一个类似的情况,但是我的用户有粉丝,而这些粉丝又有很多粉丝,我需要递归地获取第一个用户的所有粉丝。 - Alejandro Reyes
我对这个解决方案有一个问题,它不能保持正确的顺序,顺序是随机的。 - Dijiflex

2

如果您可以使用客户端JavaScript,您可以在Mongo shell上使用递归来实现此目的:

var pushAncesstors = function (name, doc) {
  if(doc.parent) {
    db.collection.update({name : name}, {$addToSet : {"ancesstors" : {name : doc.parent}}});
    pushAncesstors(name, db.collection.findOne({name : doc.parent}))
  }
}

db.collection.find().forEach(function (doc){
  pushAncesstors(doc.name, doc);
})

这将为您提供所有产品的完整层次结构。输出示例:
{ "_id" : "1", "name" : "A", "parent" : "" }
{ "_id" : "2", "name" : "B", "parent" : "A", "ancesstors" : [ { "name" : "A" } ] }
{ "_id" : "3", "name" : "C", "parent" : "B", "ancesstors" : [ { "name" : "B" }, { "name" : "A" } ] }
{ "_id" : "4", "name" : "D", "parent" : "C", "ancesstors" : [ { "name" : "C" }, { "name" : "B" }, { "name" : "A" } ] }

如果您的要求不是在正确的集合中更新数据,可以将数据插入到另一个集合中并在那里进行更新。 pushAncesstors函数将更改为:

var pushAncesstors = function (name, doc) {
  if(doc.parent) {
    db.outputColl.save(doc)
    db.outputColl.update({name : name}, {$addToSet : {"ancesstors" : {name : doc.parent}}});
    pushAncesstors(name, db.collection.findOne({name : doc.parent}))
  }
}

谢谢您的回答。是的,我可以使用客户端JavaScript。但是,上面的代码会更新现有文档,对吗?需要的是获取层次结构,而不是更新文档。 - RaR
更新答案以保留当前集合不被修改。 - ares
这是将数据带入逻辑(代码)的经典例子,这非常昂贵。我建议您使用Mongo API(廉价操作)将您的逻辑(代码)发送到数据。 - Ravi Soni

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