从输入对象动态查询

时间:2017-07-07 14:53:46

标签: mongodb mongodb-query aggregation-framework

我试图动态查询如下所示的数据库:

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. 哪些代码会产生所需的结果?
  5. 目前正在使用mongoDB 3.4.4

2 个答案:

答案 0 :(得分: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"
      }
    }
  }
]  

答案 1 :(得分:2)

这里有几个问题。首先,输入值中的数组参数应该与$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那样,但也将输出放入单独的文档中,而不是尝试组合成“one”,这可能不是最佳实践,并且可能在极端情况下打破BSON限制为16MB。因此,以这种方式获得“不同”通常会更好。