【问题标题】:Dynamically Querying From an Input Object从输入对象动态查询
【发布时间】:2017-12-11 22:24:08
【问题描述】:

我正在尝试动态查询如下所示的数据库:

db.test.insert({
    "_id" : ObjectId("58e574a768afb6085ec3a388"),
    "place": "A",
    "tests" : [
        {
            "name" : "1",
            "thing" : "X",
            "evaluation" : [
                {
                    "_id": ObjectId("58f782fbbebac50d5b2ae558"),
                    "aHigh" : [1,2],
                    "aLow" : [ ],
                    "zHigh" : [ ],
                    "zLow" : [1,3]
                },
                {
                    "_id": ObjectId("58f78525bebac50d5b2ae5c9"),
                    "aHigh" : [1,4],
                    "aLow" : [2],
                    "zHigh" : [ 3],
                    "zLow" : [ ]
                },
                {
                    "_id": ObjectId("58f78695bebac50d5b2ae60e"),
                    "aHigh" : [ ],
                    "aLow" : [1,2,3],
                    "zHigh" : [1,2,3,4],
                    "zLow" : [ ]
                },]
            },
            {
            "name" : "1",
            "thing" : "Y",
            "evaluation" : [
                {
                    "_id": ObjectId("58f78c37bebac50d5b2ae704"),
                    "aHigh" : [1,3],
                    "aLow" : [4],
                    "zHigh" : [ ],
                    "zLow" : [3]
                },
                {
                    "_id": ObjectId("58f79159bebac50d5b2ae75c"),
                    "aHigh" : [1,3,4],
                    "aLow" : [2],
                    "zHigh" : [2],
                    "zLow" : [ ]
                },
                {
                    "_id": ObjectId("58f79487bebac50d5b2ae7f1"),
                    "aHigh" : [1,2,3],
                    "aLow" : [ ],
                    "zHigh" : [ ],
                    "zLow" : [1,2,3,4]
                },]
            }
            ]
        })
db.test.insert({
    "_id" : ObjectId("58eba09e51f7f631dd24aa1c"),
    "place": "B",
    "tests" : [
        {
            "name" : "2",
            "thing" : "Y",
            "evaluation" : [
                {
                    "_id": ObjectId("58f7879abebac50d5b2ae64f"),
                    "aHigh" : [2],
                    "aLow" : [3 ],
                    "zHigh" : [ ],
                    "zLow" : [1,2,3,4]
                },
                {
                    "_id": ObjectId("58f78ae1bebac50d5b2ae6db"),
                    "aHigh" : [ ],
                    "aLow" : [ ],
                    "zHigh" : [ ],
                    "zLow" : [3,4]
                },
                {
                    "_id": ObjectId("58f78ae1bebac50d5b2ae6dc"),
                    "aHigh" : [1,2],
                    "aLow" : [3,4],
                    "zHigh" : [ ],
                    "zLow" : [1,2,3,4]
                },]
            }
            ]
        })

为了查询数据库,我有一个由程序的另一部分创建的对象。它的形式是:

var outputObject = {
    "top": {
        "place": [
        "A"
        ]
    },
    "testing": {
        "tests": {
            "name": [
                "1",
            ],
            "thing": [
                "X",
                "Y"
            ]
        }
    }
    }

然后我在聚合框架中使用 outputObject$match 语句来执行查询。我包含了两个似乎不起作用的查询。

db.test.aggregate([
        {$match: {outputObject.top}},
        {$unwind: '$tests'},
        {$match: {outputObject.testing}},
        {$unwind: '$tests.evaluation'},
        {$group: {_id: null, uniqueValues: {$addToSet: "$tests.evaluation._id"}}}
    ])

db.test.aggregate([
        {$match: {$and: [outputObject.top]}},
        {$unwind: '$tests'},
        {$match: {$and: [outputObject.testing]}},
        {$unwind: '$tests.evaluation'},
        {$group: {_id: null, uniqueValues: {$addToSet: "$tests.evaluation._id"}}}
    ])

但是,这种方法似乎不起作用。我有几个问题:

  1. 在将对象outputObject 应用到$match 语句之前,是否需要修改它?
  2. 我的查询是否正确?
  3. 我应该将$and$in$match 语句结合使用吗?
  4. 什么代码会产生想要的结果?

目前使用mongoDB 3.4.4

【问题讨论】:

    标签: mongodb mongodb-query aggregation-framework


    【解决方案1】:

    这里有几个问题。首先,您的输入值中的数组参数应该与$in 进行比较,其中许多“列表中的任何一个”以匹配。

    第二个问题是,由于这里的路径是“嵌套的”,因此您实际上需要转换为"dot notation",否则您有第一个问题的另一个变体,条件将在"test" 数组中查找元素具有您在输入中指定的提供字段。

    所以除非你也“点”路径,因为你的数组项还包含输入中未提供的"evaluation",那么它也不会匹配。

    这里的另一个问题,但很容易纠正,这里实际上不需要 "top""testing" 分隔。这两个条件实际上都适用于管道中的“两个”$match 阶段。因此,您实际上可以将其“展平”,如示例所示:

    var outputObject = {
            "top" : {
                    "place" : [
                            "A"
                    ]
            },
            "testing" : {
                    "tests" : {
                            "name" : [
                                    "1"
                            ],
                            "thing" : [
                                    "X",
                                    "Y"
                            ]
                    }
            }
    };
    
    function dotNotate(obj,target,prefix) {
      target = target || {},
      prefix = prefix || "";
    
      Object.keys(obj).forEach(function(key) {
        if ( Array.isArray( obj[key] ) ) {
          return target[prefix + key] = { "$in": obj[key] };
        } else if ( typeof(obj[key]) === "object" ) {
          dotNotate(obj[key],target,prefix + key + ".");
        } else {
          return target[prefix + key] = obj[key];
        }
      });
    
      return target;
    }
    
    // Run the transformation
    var queryObject = dotNotate(Object.assign(outputObject.top,outputObject.testing));
    

    这会产生queryObject,现在看起来像:

    {
        "place" : {
            "$in" : [ 
                "A"
            ]
        },
        "tests.name" : {
            "$in" : [ 
                "1"
            ]
        },
        "tests.thing" : {
            "$in" : [ 
                "X", 
                "Y"
            ]
        }
    }
    

    然后你就可以运行聚合了:

    db.test.aggregate([
      { '$match': queryObject },
      { '$unwind': "$tests" },
      { '$match': queryObject },
      { '$unwind': "$tests.evaluation" },
      { '$group': {
        '_id': null,
        'uniqueValues': {
          '$addToSet': "$tests.evaluation._id"
        }
      }}
    ])
    

    正确过滤对象

    {
        "_id" : null,
        "uniqueValues" : [ 
            ObjectId("58f79487bebac50d5b2ae7f1"), 
            ObjectId("58f79159bebac50d5b2ae75c"), 
            ObjectId("58f782fbbebac50d5b2ae558"), 
            ObjectId("58f78c37bebac50d5b2ae704"), 
            ObjectId("58f78525bebac50d5b2ae5c9"), 
            ObjectId("58f78695bebac50d5b2ae60e")
        ]
    }
    

    请注意,您在此处提供的条件实际上与您在问题中提供的所有文档和数组条目都匹配。但它当然会实际删除任何不匹配的内容。

    理想情况下,“初始”查询宁愿使用$elemMatch

    {
        "place" : {
            "$in" : [ 
                "A"
            ]
        },
        "tests": {
          "$elemMatch": {
            "name" : { "$in" : [ "1" ] },
            "thing" : { "$in" : [ "X", "Y" ] }
          }
        }
    }
    

    这实际上会在初始查询阶段正确过滤所有文档,因为它只会选择实际上具有数组元素的文档,这些元素实际上“仅”匹配那些条件,而不是“初始”中的点表示形式" 查询也将返回文档,其中 "test" 数组的标记条件在“任何元素”而不是元素上的“两个条件”中得到满足。但这可能是另一个需要考虑的练习,因为重组后的查询可以在没有 $elemMatch 的情况下应用于初始过滤器和“内部”过滤器。


    实际上感谢this nice solution to a "Deep Object Merge" 没有额外的库依赖,您可以像这样使用$elemMatch

    var outputObject = {
            "top" : {
                    "place" : [
                            "A"
                    ]
            },
            "testing" : {
                    "tests" : {
                            "name" : [
                                    "1"
                            ],
                            "thing" : [
                                    "X",
                                    "Y"
                            ]
                    }
            }
    };
    
    function dotNotate(obj,target,prefix) {
      target = target || {},
      prefix = prefix || "";
    
      Object.keys(obj).forEach(function(key) {
        if ( Array.isArray( obj[key] ) ) {
          return target[prefix + key] = { "$in": obj[key] };
        } else if ( typeof(obj[key]) === "object" ) {
          dotNotate(obj[key],target,prefix + key + ".");
        } else {
          return target[prefix + key] = obj[key];
        }
      });
    
      return target;
    }
    
    function isObject(item) {
      return (item && typeof item === 'object' && !Array.isArray(item));
    }
    
    function mergeDeep(target, ...sources) {
      if (!sources.length) return target;
      const source = sources.shift();
    
      if (isObject(target) && isObject(source)) {
        for (var key in source) {
          if (isObject(source[key])) {
            if (!target[key]) Object.assign(target, { [key]: {} });
            mergeDeep(target[key], source[key]);
          } else {
            Object.assign(target, { [key]: source[key] });
          }
        }
      }
    
      return mergeDeep(target, ...sources);
    }
    
    var queryObject = dotNotate(Object.assign(outputObject.top,outputObject.testing));
    
    // Replace dot with $elemMatch
    var initialQuery = Object.keys(queryObject).map( k => (
      ( k.split(/\./).length > 1 )
       ? { [k.split(/\./)[0]]: { "$elemMatch": { [k.split(/\./)[1]]: queryObject[k] } } }
       : { [k]: queryObject[k] }
    )).reduce((acc,curr) => mergeDeep(acc,curr),{})
    
    db.test.aggregate([
      { '$match': initialQuery },
      { '$unwind': "$tests" },
      { '$match': queryObject },
      { '$unwind': "$tests.evaluation" },
      { '$group': {
        '_id': null,
        'uniqueValues': {
          '$addToSet': "$tests.evaluation._id"
        }
      }}
    ])
    

    将管道发送到服务器:

    [
        {
            "$match" : {
                "place" : {
                    "$in" : [ 
                        "A"
                    ]
                },
                "tests" : {
                    "$elemMatch" : {
                        "name" : {
                            "$in" : [ 
                                "1"
                            ]
                        },
                        "thing" : {
                            "$in" : [ 
                                "X", 
                                "Y"
                            ]
                        }
                    }
                }
            }
        },
        {
            "$unwind" : "$tests"
        },
        {
            "$match" : {
                "place" : {
                    "$in" : [ 
                        "A"
                    ]
                },
                "tests.name" : {
                    "$in" : [ 
                        "1"
                    ]
                },
                "tests.thing" : {
                    "$in" : [ 
                        "X", 
                        "Y"
                    ]
                }
            }
        },
        {
            "$unwind" : "$tests.evaluation"
        },
        {
            "$group" : {
                "_id" : null,
                "uniqueValues" : {
                    "$addToSet" : "$tests.evaluation._id"
                }
            }
        }
    ]
    

    另外,您的$group 可能最好写成:

    { "$group": { "_id": "$tests.evaluation._id" } }
    

    它返回“distinct”,就像$addToSet 一样,但也将输出放入单独的文档中,而不是尝试组合成“一个”,这可能不是最佳实践,并且在极端情况下可能会打破 BSON 限制16MB。因此,通常以这种方式获得“distinct”会更好。

    【讨论】:

    • 使用您的解决方案效果很好,除非top 中没有值。例如,当var outputObject = { "testing" : { "tests" : { "name" : [ "1" ], "thing" : [ "X", "Y" ] } } }; 解决方案失败。不管top中是否有东西,有没有办法让它发挥作用?
    • @black_sheep07 如果您真的阅读了内容,我会两次告诉您,您需要单独的“顶部”和“底部”部分的假设是不正确的,实际上您需要在最初和以后都应用这些条件过滤。我还记得之前和你谈过when you have a different question to what you ask then you ask it separately instead
    • @black_sheep07 换句话说,这是一个“代码中的单一语句”Object.assign(outputObject.top,outputObject.testing),这是唯一引用我说你应该“摆脱”的两个键的东西,因为没有必要让他们在那里。实际上,该语句所做的是通过将两个键的结果“合并”为单个文档来“删除”这些键。
    • 我最初不明白您所说的展平是什么意思,因为您说您使用了展平的数据集,但您示例中使用的outputObject 与我使用的相同。但是,通过您的额外解释,我明白了。我可以修改传入数据集的结构,使其不依赖于现有的.top。感谢您的所有帮助。
    【解决方案2】:

    最好同意outputObject的固定格式并相应地编写聚合查询。

    您现在可以处理 outputObject 以注入查询运算符并转换键以匹配字段。

    如下所示。

    {
        "top": {
          "place": {
            "$in": [
              "A"
            ]
          }
        },
        "testing": {
          "tests.name": {
            "$in": [
              "1"
            ]
          },
          "tests.thing": {
            "$in": [
              "X",
              "Y"
            ]
          }
        }
      }
    

    JS 代码

    var top = outputObject.top;
    Object.keys(top).forEach(function(a) {
        top[a] = {
            "$in": top[a]
        };
    });
    
    var testing = outputObject.testing;
    Object.keys(testing).forEach(function(a) {
        Object.keys(testing[a]).forEach(function(b) {
            var c = [a + "." + b];
            testing[c] = {
                "$in": testing[a][b]
            };
        })
        delete testing[a];
    });
    

    您现在可以使用聚合查询

    db.test.aggregate([{
            $match: top
        },
        {
            $unwind: "$tests"
        },
        {
            $match: testing
        },
        {
            $unwind: "$tests.evaluation"
        },
        {
            $group: {
                _id: null,
                uniqueValues: {
                    $addToSet: "$tests.evaluation._id"
                }
            }
        }
    ])
    

    您可以重构代码以在 3.4 中使用以下聚合管道

    将您的输出对象(包括$in 运算符)处理为

    {
      "top": {
        "place": {
          "$in": [
            "A"
          ]
        }
      },
      "testing": {
        "tests": {
          "name": [
            "1"
          ],
          "thing": [
            "X",
            "Y"
          ]
        }
      }
    };
    

    JS 代码

    var top = outputObject.top;
    Object.keys(top).forEach(function(a) {top[a] = {"$in":top[a]};});
    

    聚合:

    [
      {
        "$match": top
      },
      {
        "$addFields": {
          "tests": {
            "$filter": {
              "input": "$$tests",
              "as": "res",
              "cond": {
                "$and": [
                  {
                    "$in": [
                      "$$res.name",
                      outputObject.testing.tests.name
                    ]
                  },
                  {
                    "$in": [
                      "$$res.thing",
                      outputObject.testing.tests.thing
                    ]
                  }
                ]
              }
            }
          }
        }
      },
      {
        "$unwind": "$tests.evaluation"
      },
      {
        "$group": {
          "_id": null,
          "uniqueValues": {
            "$addToSet": "$tests.evaluation._id"
          }
        }
      }
    ]  
    

    【讨论】:

    • 不幸的是,对象不是固定的。有时,它会是var outputObject = { "top": { "place": [ "A" ] }, "testing": { "tests": { "name": [ "1" ] } } },并且查询的组件要多得多。我无法手动将它们添加在一起。
    • 您可以动态创建整个管道。您可以先处理输出对象字段并添加必要的查询运算符,然后根据输出字段调整聚合阶段并组装到聚合管道中。这是一个这样的例子stackoverflow.com/questions/43889978/…
    • 我正在查看示例,似乎我将不得不为每个级别的查询创建一个运算符。例如,我必须为testing.tests.name 创建一个,为testing.tests.thing 创建另一个,为top.place 创建另一个。这准确吗?
    • 这真的取决于聚合查询。我为您提供的查询不需要testing 部分的运算符,但需要匹配查询部分的运算符。只要您同意一种固定的输出字段格式,您就可以根据您的查询进行调整。
    • 我已经更新了答案以包含 javascript 代码来转换您的 outputObject 以用于您的查询和提供的答案。这应该让您对如何处理动态查询有所了解。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-28
    • 1970-01-01
    相关资源
    最近更新 更多