Запросите и отфильтруйте документ ChangeStream с помощью «пунктирного поля» '.' - PullRequest
1 голос
/ 03 октября 2019

У меня есть документ, который выглядит как

{
  components: { weapon: { type: "Sword" }, health: { value: 10 } }
  type: "Monster"
}

Я использую поток изменений , который возвращает

{
  operationType: 'update',
  updateDescription: {
    updatedFields: { 'components.weapon': [Object] }
  }
}

Я хочу добавить запрос вконвейер агрегации для фильтрации любых обновлений, которые не для компонентов, т. е. если поле типа обновлено, я не хочу получать обновление.

Мой запрос выглядит как

{ $match: { "updateDescription.updatedFields.components.weapon": { $exists: true } }

однако это не работает, так как поле на updatedFields - это 'компоненты.получение', а не компоненты: {оружие: ..}.

Если бы я мог использовать скобочную запись, я бы сделал

{ $match: { "updateDescription.updatedFields['components.weapon']": { $exists: true } }

однако это не разрешено в синтаксисе MongoDB (или, по крайней мере, не работает).

Есть ли решение для этого?

1 Ответ

0 голосов
/ 03 октября 2019

Это сложно понять, но здесь есть несколько веских причин, и случай документа normal MongoDB не будет включать такие "точечные поля" . Таким образом, требуется особая обработка.

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

Короткий случай: вам нужно выражение pipeline для вашего watch(), например:

const pipeline = [
  { "$match": {
    "$expr": {
      "$ne": [
        { "$size": {
          "$filter": {
            "input": {
              "$objectToArray": "$updateDescription.updatedFields"
            },
            "cond": {
              "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
            }
          }
        }},
        0
      ]
    }
  }}
];

Для этого используется $objectToArray для преобразования объекта updatedFields в массив свойств k и v вместо именованных ключей. На этом этапе результирующие значения будут выглядеть следующим образом:

[
  {
    "k": "coponents.weapon",
    "v": "Sword"
  }
]

Это позволяет использовать массив now с операцией $filter, используя выражение из$indexOfCP, который проверяет наличие строки в значении свойства k. Если это что-то отличное от -1 (для не найдено ), то любой элемент, содержащий "component" в этом значении, будет единственным сохраненным элементом и соответствующим элементом из массива результата.

ПРИМЕЧАНИЕ Если у вас MongoDB 4.2, вы можете обратиться к оператору $regexMatch вместо $indexOfCP. Это не должно быть необходимо для простого тестирования «присутствия» строки в строке, но Регулярные выражения, конечно, могут сделать немного больше, если ваш сценарий использования этого требует. от начала строки до включенной "точки", заменяя cond на $filter:

     "$not": { "$regexMatch": { "input": "$$this.k", "regex": /^components\./ } }

, так как он возвращается в виде массива,Затем вы можете протестировать $size, чтобы увидеть, действительно ли в массиве отфильтрованный действительно осталось что-то после удаления значений "component". Если это не так и размер действительно 0, то результаты отбрасываются через $expr, который также является основным оператором, позволяющим использовать выражения агрегации в пределах $match также.

Конечно, все, что делает, это выбирает документы , которые действительно действительны для возврата. В случае, если у вас могут быть другие измененные поля в результате update, указанном в документе «Поток изменений», тогда вам фактически нужно применить тот же тип операции $filter, чтобы фактически удалить"component" поля из результата:

  { "$addFields": {
      "updateDescription": {
        "updatedFields": {
          "$arrayToObject": {
            "$filter": {
              "input": {
                "$objectToArray": "$updateDescription.updatedFields"
              },
              "cond": {
                "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ] }, -1 ]
              }
            }
          }
        }
      }
    }
  }

Обратите внимание, здесь добавлено $arrayToObject, которое по существу полностью изменяет используемый процесс манипуляции и затем фактически возвращаетupdatedFields содержимое в исходном виде, без нежелательных ключей.


Для демонстрации приведен фактический полный список, который воспроизводит изменения, внесенные в коллекцию с такой структурой, и включает watcher конвейер для отключения нежелательных изменений:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const options = { useNewUrlParser: true, useUnifiedTopology: true };

const log = doc => console.log(JSON.stringify(doc, undefined, 2));

(async function() {


  try {

    let client = await MongoClient.connect(uri, options);

    let db = client.db('test');


    // Insert some starting data
    await db.collection('things').deleteMany();
    await db.collection('things').insertOne({
      components: {
        weapon: { type: "Sword" },
        health: { value: 10 }
      },
      type: "Monster"
    });

    // Set up the changeStream
    const pipeline = [
      // Filters documents
      { "$match": {
        "$expr": {
          "$ne": [
            { "$size": {
              "$filter": {
                "input": {
                  "$objectToArray": "$updateDescription.updatedFields"
                },
                "cond": {
                  "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
                  /* Alternate MongoDB 4.2 syntax
                  "$not": {
                    "$regexMatch": {
                      "input": "$$this.k",
                      "regex": /^components\./
                    }
                  }
                  */
                }
              }
            }},
            0
          ]
        }
      }},
      /* -- Uncomment just to see the k and v structure
      { "$project": {
        "update": { "$objectToArray": "$updateDescription.updatedFields" }
      }}
      */

      // Actually removes the keys and returns only non filtered
      { "$addFields": {
          "updateDescription": {
            "updatedFields": {
              "$arrayToObject": {
                "$filter": {
                  "input": {
                    "$objectToArray": "$updateDescription.updatedFields"
                  },
                  "cond": {
                    "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ] }, -1 ]
                    /* Alternate MongoDB 4.2 syntax
                    "$not": {
                      "$regexMatch": {
                        "input": "$$this.k",
                        "regex": /^components\./
                      }
                    }
                    */
                  }
                }
              }
            }
          }
        }
      }
    ];

    const changeStream = db.collection('things').watch(pipeline);

    changeStream.on('change', next => log({ changeDocument: next }));


    // Loop some changes


    await new Promise(async (resolve, reject) =>  {

      let tick = true;

      setInterval(async () => {
        try {
          let { value }= await db.collection('things')
            .findOneAndUpdate(
              {},
              { $set: { 'components.weapon': (tick) ? 'Knife' : 'Sword' }},
              { returnOriginalDocument: false }
            );
          tick = !tick; // flip the boolean
          log({ currentDoc: value });
        } catch (e) {
          reject(e);
        }
      },2000)

    });



  } catch (e) {
    console.error(e)
  }


})()
...