【问题标题】:Query and Filter a ChangeStream Document with a "dotted field" '.'使用“虚线字段”'.'查询和过滤 ChangeStream 文档
【发布时间】:2019-10-03 02:41:25
【问题描述】:

我有一个看起来像这样的文档

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

我正在使用正在返回的change stream

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

我想向聚合管道添加一个查询,以过滤掉任何不是组件的更新,即如果类型字段已更新,我不希望获得更新。

我的查询看起来像

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

但这不起作用,因为 updatedFields 上的字段是 'components.weapon' 而不是 components: { Weapon: .. }。

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

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

但是这在 MongoDB 语法中是不允许的(或者至少它不起作用)。

有解决办法吗?

【问题讨论】:

    标签: node.js mongodb mongodb-query aggregation-framework


    【解决方案1】:

    这是一个棘手的问题,但这里有一些充分的理由,正常 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 以外的任何内容(对于 not found ),那么在该值中包含 "component" 的任何元素都将是唯一保留的内容,并且匹配结果数组中的元素。

    注意如果您有 MongoDB 4.2,您可能需要查看 $regexMatch 运算符来代替 $indexOfCP。不需要简单地测试字符串中字符串的“存在”,但是如果您的用例需要,正则表达式当然可以做更多的事情。

    在这里合适,您可能会从字符串的开头搜索到包含的“点”,将cond 中的$filter 替换为:

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

    由于它是作为数组返回的,因此您可以测试 $size 以查看现在 已过滤 数组在删除 "component" 值后是否实际上还剩下任何东西.如果不是,并且大小确实为0,则通过$expr 丢弃结果,这也是允许在$match 中使用聚合表达式的主要运算符。

    当然,所做的只是选择文档,这些文档实际上可以返回。如果您在 Change Stream 文档中指示的 update 结果中可能有其他更改的字段,那么您实际上需要对其使用相同类型的 $filter 操作,以便从结果:

      { "$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)
      }
    
    
    })()
    

    【讨论】:

    • 精彩而详细的答案,这阐明了 MongoDB 操作员如何为我工作。谢谢你,尼尔。
    猜你喜欢
    • 1970-01-01
    • 2019-03-21
    • 1970-01-01
    • 2019-07-11
    • 1970-01-01
    • 2017-09-26
    • 2018-05-11
    • 2019-10-17
    • 1970-01-01
    相关资源
    最近更新 更多