I have a list of documents in MongoDB with tree structure, where Model Tree Structures with Parent References pattern used. I want a single aggregation query which returns ancestor list(till the root), given the 'name' property.
Structure:
{
'_id': '1',
'name': 'A',
'parent': '',
},
{
'_id': '2',
'name': 'B',
'parent': 'A',
},
{
'_id': '3',
'name': 'C',
'parent': 'B',
},
{
'_id': '4',
'name': 'D',
'parent': 'C',
}
Aggregation result:(Given, name = 'D')
{
'_id': '4',
'name': 'D',
'ancestors': [{name:'C'}, {name:'B'}, {name:'A'}]
}
Note:
I can't change the document structure now. It will cause many problems. I saw many solutions which suggest to use Model Tree Structures with an Array of Ancestors. But I cannot use it now. Is there any way to achieve it with the above pattern using single aggregation query? Thank you
Starting from MongoDB 3.4, we can do this with the Aggregation Framework.
The first and most important stage in our pipeline is the $graphLookup
stage. $graphLookup
allows us to recursively match on the "parent" and "name" field. As result, we get the ancestors of each "name".
The next stage in the pipeline is the $match
stage where we simply select the "name" we are interested in.
The final stage is the $addFields
or $project
stage where we apply an expression to the "ancestors" array using the $map
array operator.
Of course with the $reverseArray
operator we reverse our array in order to get the expected result.
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" }
}
}
}
}}
]
)
If you are open to use client side javascript, you can use recursion on the mongo shell to achieve this:
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);
})
This will give you complete hirearchy for all products. Sample output:
{ "_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" } ] }
If your requirement is not to update the correct collection, insert the data in a diffferent collection and update there. The pushAncesstors
function will change to:
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}))
}
}