Node.js 查询并筛选带有“点域”的ChangeStream文档。'

Node.js 查询并筛选带有“点域”的ChangeStream文档。',node.js,mongodb,mongodb-query,aggregation-framework,Node.js,Mongodb,Mongodb Query,Aggregation Framework,我有一份文件看起来像 { components: { weapon: { type: "Sword" }, health: { value: 10 } } type: "Monster" } { $match: { "updateDescription.updatedFields.components.weapon": { $exists: true } } 我正在使用一个正在返回的 { operationType: 'update', updateDescription:

我有一份文件看起来像

{
  components: { weapon: { type: "Sword" }, health: { value: 10 } }
  type: "Monster"
}
{ $match: { "updateDescription.updatedFields.components.weapon": { $exists: true } }
我正在使用一个正在返回的

{
  operationType: 'update',
  updateDescription: {
    updatedFields: { 'components.weapon': [Object] }
  }
}
我想向聚合管道添加一个查询,以筛选出不适用于组件的任何更新,即,如果类型字段已更新,我不希望获得更新

我的问题看起来像

{
  components: { weapon: { type: "Sword" }, health: { value: 10 } }
  type: "Monster"
}
{ $match: { "updateDescription.updatedFields.components.weapon": { $exists: true } }
但是,这不起作用,因为updatedFields上的字段是“components.wearm”而不是components:{wearm:..}

如果我能使用括号符号,我会这样做

{ $match: { "updateDescription.updatedFields['components.weapon']": { $exists: true } }
但是,MongoDB语法中不允许这样做,或者至少它不起作用


有解决方案吗?

这是一个棘手的问题,但这里有一些确凿的原因,正常的MongoDB文档案例不包括这样的虚线字段。所以确实需要一些特殊的处理

这里的基本情况是,您实际上需要将文档中包含虚线字段的键转换为字符串,然后只需查找该字符串中是否存在组件

简而言之,您基本上需要一个管道表达式,如下所示:

const pipeline = [
  { "$match": {
    "$expr": {
      "$ne": [
        { "$size": {
          "$filter": {
            "input": {
              "$objectToArray": "$updateDescription.updatedFields"
            },
            "cond": {
              "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
            }
          }
        }},
        0
      ]
    }
  }}
];
[
  {
    "k": "coponents.weapon",
    "v": "Sword"
  }
]
这样做的目的是将updatedFields对象转换为k和v属性数组,而不是命名键。此时,结果值如下所示:

const pipeline = [
  { "$match": {
    "$expr": {
      "$ne": [
        { "$size": {
          "$filter": {
            "input": {
              "$objectToArray": "$updateDescription.updatedFields"
            },
            "cond": {
              "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ], }, -1]
            }
          }
        }},
        0
      ]
    }
  }}
];
[
  {
    "k": "coponents.weapon",
    "v": "Sword"
  }
]
这允许now数组与使用表达式的操作一起使用,从该表达式可以测试k属性值中是否存在字符串。如果它不是-1 for not found,那么该值中包含组件的任何元素都将是唯一保留的元素,并且匹配结果数组中的元素

注意:如果您有MongoDB 4.2,您可能希望查看操作符,而不是。简单地测试字符串中是否存在字符串不应该是必需的,但是如果您的用例需要,正则表达式当然可以做得更多

在这里,您可能会从字符串的开头一直搜索到包含的点,并替换以下内容的内部条件:

因为它是作为数组返回的,所以您可以测试,以便在删除组件值后,查看现在过滤的数组中是否还剩下任何内容。如果没有,并且大小确实为0,则通过丢弃结果,这也是允许在中使用聚合表达式的主运算符

当然,所要做的只是选择实际可返回的有效文档。如果您可能在变更流文档中指示的更新结果中有其他变更字段,那么您实际上需要对其采用相同类型的操作,以便从结果中实际删除组件字段:

  { "$addFields": {
      "updateDescription": {
        "updatedFields": {
          "$arrayToObject": {
            "$filter": {
              "input": {
                "$objectToArray": "$updateDescription.updatedFields"
              },
              "cond": {
                "$eq": [{ "$indexOfCP": [ "$$this.k", "component" ] }, -1 ]
              }
            }
          }
        }
      }
    }
  }
请注意,这里的添加实质上颠倒了所使用的操作过程,然后实际返回updatedFields内容的原始形式,没有不需要的键

为了演示,下面是一个实际的完整列表,它复制了对具有这种结构的集合所做的更改,并包括观察者管道,以便使不需要的更改静音:

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)
  }


})()

精彩而详细的回答,这说明了MongoDB操作符是如何为我工作的。谢谢你,尼尔。