Соответствие ObjectId String для $ graphLookup - PullRequest
0 голосов
/ 18 мая 2018

Я пытаюсь запустить $graphLookup, как показано ниже:

$graphLookup sample

Цель состоит в том, чтобы, учитывая конкретную запись (прокомментировал $match там), извлеките его полный «путь» через immediateAncestors свойство.Как видите, этого не происходит.

Я ввел $convert здесь, чтобы иметь дело с _id из коллекции как string, полагая, что можно было бы "сопоставить" с _id из immediateAncestors список записей (это string).

Итак, я выполнил еще один тест с другими данными (без ObjectId s):

db.nodos.insert({"id":5,"name":"cinco","children":[{"id":4}]})
db.nodos.insert({"id":4,"name":"quatro","ancestors":[{"id":5}],"children":[{"id":3}]})
db.nodos.insert({"id":6,"name":"seis","children":[{"id":3}]})
db.nodos.insert({"id":1,"name":"um","children":[{"id":2}]})
db.nodos.insert({"id":2,"name":"dois","ancestors":[{"id":1}],"children":[{"id":3}]})
db.nodos.insert({"id":3,"name":"três","ancestors":[{"id":2},{"id":4},{"id":6}]})
db.nodos.insert({"id":7,"name":"sete","children":[{"id":5}]})

И запрос:

db.nodos.aggregate( [
  { $match: { "id": 3 } },
  { $graphLookup: {
      from: "nodos",
      startWith: "$ancestors.id",
      connectFromField: "ancestors.id",
      connectToField: "id",
      as: "ANCESTORS_FROM_BEGINNING"
    }
  },
  { $project: {
      "name": 1,
      "id": 1,
      "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING.id"
    }
  }
] )

... который выводит то, что я ожидал (пять записей, прямо и косвенно связанных с одной с id 3):

{
    "_id" : ObjectId("5afe270fb4719112b613f1b4"),
    "id" : 3.0,
    "name" : "três",
    "ANCESTORS_FROM_BEGINNING" : [ 
        1.0, 
        4.0, 
        6.0, 
        5.0, 
        2.0
    ]
}

Вопрос: естьспособ достичь цели, о которой я говорил в начале?

Я использую Mongo 3.7.9 (из официального Docker)

Заранее спасибо!

1 Ответ

0 голосов
/ 18 мая 2018

В настоящее время вы используете версию MongoDB для разработки, в которой включены некоторые функции, которые, как ожидается, будут выпущены вместе с MongoDB 4.0 в качестве официального выпуска.Обратите внимание, что некоторые функции могут быть изменены до окончательного выпуска, поэтому производственный код должен знать об этом, прежде чем совершать это.

Почему здесь не удается преобразовать $

Вероятно, лучший способОбъясните это, чтобы посмотреть на ваш измененный образец, но заменив ObjectId значениями для _id и "строками" для тех, кто находится под массивами:

{
  "_id" : ObjectId("5afe5763419503c46544e272"),
   "name" : "cinco",
   "children" : [ { "_id" : "5afe5763419503c46544e273" } ]
},
{
  "_id" : ObjectId("5afe5763419503c46544e273"),
  "name" : "quatro",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e272" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e274"),
  "name" : "seis",
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e275"),
  "name" : "um",
  "children" : [ { "_id" : "5afe5763419503c46544e276" } ]
}
{
  "_id" : ObjectId("5afe5763419503c46544e276"),
  "name" : "dois",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e275" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e277"),
  "name" : "três",
  "ancestors" : [
    { "_id" : "5afe5763419503c46544e273" },
    { "_id" : "5afe5763419503c46544e274" },
    { "_id" : "5afe5763419503c46544e276" }
  ]
},
{ 
  "_id" : ObjectId("5afe5764419503c46544e278"),
  "name" : "sete",
  "children" : [ { "_id" : "5afe5763419503c46544e272" } ]
}

Это должно датьОбщая симуляция того, с чем вы пытались работать.

То, что вы пытались сделать, - преобразовать значение _id в «строку» с помощью $project перед вводом $graphLookup этап.Причина, по которой это не удается, заключается в том, что при первоначальном $project «внутри» этого конвейера, проблема в том, что источник для $graphLookup в опции "from" по-прежнемунеизменной коллекции, и поэтому вы не получите правильные данные о последующих итерациях «поиска».

db.strcoll.aggregate([
  { "$match": { "name": "três" } },
  { "$addFields": {
    "_id": { "$toString": "$_id" }
  }},
  { "$graphLookup": {
    "from": "strcoll",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Не совпадает с «поиском», поэтому:

{
        "_id" : "5afe5763419503c46544e277",
        "name" : "três",
        "ANCESTORS_FROM_BEGINNING" : [ ]
}

«Исправление»"проблема

Однако это основная проблема, а не сбой $convert или его псевдонимов.Чтобы заставить это работать на самом деле, мы можем вместо этого создать «представление» , которое представляет собой коллекцию для ввода.

Я сделаю это наоборот и преобразую«строки» в ObjectId через $toObjectId:

db.createView("idview","strcoll",[
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
])

Использование «view» однако означает, что данныепоследовательно видно с преобразованными значениями.Итак, следующая агрегация с использованием представления:

db.idview.aggregate([
  { "$match": { "name": "três" } },
  { "$graphLookup": {
    "from": "idview",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Возвращает ожидаемый результат:

{
    "_id" : ObjectId("5afe5763419503c46544e277"),
    "name" : "três",
    "ANCESTORS_FROM_BEGINNING" : [
        ObjectId("5afe5763419503c46544e275"),
        ObjectId("5afe5763419503c46544e273"),
        ObjectId("5afe5763419503c46544e274"),
        ObjectId("5afe5763419503c46544e276"),
        ObjectId("5afe5763419503c46544e272")
    ]
}

Устранение проблемы

С учетом всего сказанного, реальная проблема здесьявляется то, что у вас есть некоторые данные, которые «похожи» на значение ObjectId и фактически действительны как ObjectId, однако они были записаны как «строка».Основная проблема для всего, что работает должным образом, состоит в том, что два «типа» не совпадают, и это приводит к несоответствию равенства при попытке «объединения».

Таким образом, реальное исправление остается таким же, кактак было всегда: вместо этого нужно просмотреть данные и исправить их так, чтобы «строки» на самом деле также были значениями ObjectId.Затем они будут соответствовать ключам _id, к которым они предназначены, и вы экономите значительный объем памяти, поскольку ObjectId занимает намного меньше места для хранения, чем его строковое представлениев шестнадцатеричных символах.

Используя методы MongoDB 4.0, вы "могли" фактически используете "$toObjectId" для того, чтобы написать новую коллекцию, во многом аналогичноНезависимо от того, что мы создали «представление» ранее:

db.strcoll.aggregate([
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
  { "$out": "fixedcol" }
])

Или, конечно, где вам «нужно» сохранить ту же коллекцию, тогда традиционный «цикл и обновление» остается таким же, как всегда:

var updates = [];

db.strcoll.find().forEach(doc => {
  var update = { '$set': {} };

  if ( doc.hasOwnProperty('children') )
    update.$set.children = doc.children.map(e => ({ _id: new ObjectId(e._id) }));
  if ( doc.hasOwnProperty('ancestors') )
    update.$set.ancestors = doc.ancestors.map(e => ({ _id: new ObjectId(e._id) }));

  updates.push({
    "updateOne": {
      "filter": { "_id": doc._id },
      update
    }
  });

  if ( updates.length > 1000 ) {
    db.strcoll.bulkWrite(updates);
    updates = [];
  }

})

if ( updates.length > 0 ) {
  db.strcoll.bulkWrite(updates);
  updates = [];
}

Это немного «кувалда» из-за того, что фактически перезаписывает весь массив за один раз.Не отличная идея для производственной среды, но достаточно для демонстрации целей этого упражнения.

Заключение

Таким образом, хотя MongoDB 4.0 добавит эти функции «кастинга», которые действительно могут быть очень полезны, их реальное намерение на самом деле не подходит для таких случаев, как этот.На самом деле они гораздо более полезны, что продемонстрировано в «преобразовании» в новую коллекцию с использованием конвейера агрегации, чем большинство других возможных применений.

Пока мы «можем» создать «представление»который преобразует типы данных, чтобы позволить таким вещам, как $lookup и $graphLookup работать там, где фактические данные сбора отличаются, это действительно всего лишь «пластырь» о реальной проблеме, поскольку типы данных на самом деле не должны различаться и должны фактически преобразовываться постоянно.

Использование «представления» фактически означает, что конвейер агрегации для построения должен эффективно запускаться каждый каждый раз, когда осуществляется доступ к «коллекции» (фактически «представление»), что создает реальные издержки.

Предотвращение накладных расходов обычно является целью проектирования, поэтому исправление таких ошибок хранения данных необходимо для достижения реальной производительности вашего приложения, а не просто для работы с "грубой силой", которая только замедлит работу.


Гораздо более безопасный сценарий "преобразования", который применял "соответствующие" обновления для каждого элемента массива.Для этого кода требуется NodeJS v10.x и последняя версия драйвера узла MongoDB 3.1.x:

const { MongoClient, ObjectID: ObjectId } = require('mongodb');
const EJSON = require('mongodb-extended-json');

const uri = 'mongodb://localhost/';

const log = data => console.log(EJSON.stringify(data, undefined, 2));

(async function() {

  try {

    const client = await MongoClient.connect(uri);
    let db = client.db('test');
    let coll = db.collection('strcoll');

    let fields = ["ancestors", "children"];

    let cursor = coll.find({
      $or: fields.map(f => ({ [`${f}._id`]: { "$type": "string" } }))
    }).project(fields.reduce((o,f) => ({ ...o, [f]: 1 }),{}));

    let batch = [];

    for await ( let { _id, ...doc } of cursor ) {

      let $set = {};
      let arrayFilters = [];

      for ( const f of fields ) {
        if ( doc.hasOwnProperty(f) ) {
          $set = { ...$set,
            ...doc[f].reduce((o,{ _id },i) =>
              ({ ...o, [`${f}.$[${f.substr(0,1)}${i}]._id`]: ObjectId(_id) }),
              {})
          };

          arrayFilters = [ ...arrayFilters,
            ...doc[f].map(({ _id },i) =>
              ({ [`${f.substr(0,1)}${i}._id`]: _id }))
          ];
        }
      }

      if (arrayFilters.length > 0)
        batch = [ ...batch,
          { updateOne: { filter: { _id }, update: { $set }, arrayFilters } }
        ];

      if ( batch.length > 1000 ) {
        let result = await coll.bulkWrite(batch);
        batch = [];
      }

    }

    if ( batch.length > 0 ) {
      log({ batch });
      let result = await coll.bulkWrite(batch);
      log({ result });
    }

    await client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

Создает и выполняет массовые операции, подобные этим для семи документов:

{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e272"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e273"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e273"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e273"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e272"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e272"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e274"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e275"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e276"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e275"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e275"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e277"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e273"
        },
        "ancestors.$[a1]._id": {
          "$oid": "5afe5763419503c46544e274"
        },
        "ancestors.$[a2]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e273"
      },
      {
        "a1._id": "5afe5763419503c46544e274"
      },
      {
        "a2._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5764419503c46544e278"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e272"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e272"
      }
    ]
  }
}
...