Рекурсивный поиск по коллекции в 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}))
  }
}