Рекурсивный поиск по коллекции в MongoDB
У меня есть список документов в MongoDB с древовидной структурой, где используется шаблон Model Tree Structures with Parent References. Я хочу один запрос агрегации, который возвращает список предков (до корня), учитывая свойство 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'}]
}
Note:
Теперь я не могу изменить структуру документа. Это вызовет множество проблем. Я видел много решений, которые предлагают использовать Структуры дерева моделей с массивом предков. Но я не могу использовать его сейчас. Есть ли способ достичь этого с помощью вышеуказанного шаблона с использованием единого запроса на агрегацию? Спасибо вам
Ответы
Ответ 1
Начиная с MongoDB 3.4, мы можем сделать это с помощью Aggregation Framework.
Первым и самым важным этапом в нашем проекте является $graphLookup
. $graphLookup
позволяет нам рекурсивно сопоставлять поля "родитель" и "имя". В результате мы получаем предков каждого "имени".
Следующий этап в конвейере - это $match
, где мы просто выбираем интересующее нас имя.
Заключительным этапом является $addFields
или $project
, где мы применяем выражение к массиву "предков", используя оператор массива $map
.
Конечно, с $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" }
}
}
}
}}
]
)
Ответ 2
Если вы открыты для использования javascript на стороне клиента, вы можете использовать рекурсию на оболочке mongo для достижения этой цели:
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}))
}
}