Mongodb 从输入对象动态查询

Mongodb 从输入对象动态查询,mongodb,mongodb-query,aggregation-framework,Mongodb,Mongodb Query,Aggregation Framework,我正在尝试动态查询如下所示的数据库: db.test.insert({ "_id" : ObjectId("58e574a768afb6085ec3a388"), "place": "A", "tests" : [ { "name" : "1", "thing" : "X", "evaluation" : [ {

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

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"
                        ]
                }
        }
};

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"
    }
  }}
])
为了查询数据库,我有一个由程序的另一部分创建的对象。其形式如下:

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"}}}
    ])
然而,这种方法似乎没有发挥作用。我有几个问题:

  • 在将对象应用于
    $match
    语句之前,是否需要修改对象
    outputObject
  • 我的问题对吗
  • 我应该在
    $match
    语句中结合使用
    $and
    还是
    $in
  • 什么代码将产生期望的结果

  • 目前使用的是
    mongoDB 3.4.4

    最好为
    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];
    });
    
    var top = outputObject.top;
    Object.keys(top).forEach(function(a) {top[a] = {"$in":top[a]};});
    
    现在可以使用聚合查询

    db.test.aggregate([{
            $match: top
        },
        {
            $unwind: "$tests"
        },
        {
            $match: testing
        },
        {
            $unwind: "$tests.evaluation"
        },
        {
            $group: {
                _id: null,
                uniqueValues: {
                    $addToSet: "$tests.evaluation._id"
                }
            }
        }
    ])
    
    您可以重构代码以使用3.4中的聚合管道

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

    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];
    });
    
    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"
          }
        }
      }
    ]  
    
    db.test.aggregate([
      { '$match': queryObject },
      { '$unwind': "$tests" },
      { '$match': queryObject },
      { '$unwind': "$tests.evaluation" },
      { '$group': {
        '_id': null,
        'uniqueValues': {
          '$addToSet': "$tests.evaluation._id"
        }
      }}
    ])
    

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

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

    因此,除非您也“点注”路径,否则由于您的数组项还包含输入中未提供的
    “求值”
    ,因此它也不会匹配

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

    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"
            ]
        }
    }
    
    然后可以运行聚合:

    [
      {
        "$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"
          }
        }
      }
    ]  
    
    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")
        ]
    }
    
    请注意,您在此处提供的条件实际上与您在问题中提供的所有文档和数组项都匹配。但它当然会删除任何不匹配的内容

    理想情况下,“初始”查询更愿意使用

    它实际上会在初始查询阶段正确过滤所有文档,因为它只会选择实际具有数组元素的文档,而数组元素实际上“仅”匹配那些条件,而不是“初始”查询中的点符号形式,它还将返回文档,其中
    的符号条件为“测试”
    数组在“任意元素”中满足,而不是元素上的“两个条件”。但这可能是另一个需要考虑的问题,因为重构的查询可以应用于初始和“内部”过滤器,而不必使用.</P>。
    实际上,由于没有额外的库依赖项,您可以这样使用:

    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"
                            ]
                    }
            }
    };
    
    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": { "_id": "$tests.evaluation._id" } }
    

    它返回“distinct”,就像它一样,但也将输出放在单独的文档中,而不是试图组合成“one”,这可能不是最佳做法,在极端情况下可能会突破16MB的BSON限制。因此,通常最好用这种方式获得“distinct”。

    不幸的是,对象不是固定的。有时,它将是
    var outputObject={“top”:{“place”:[“A”]},“testing”:{“tests”:{“name”:[“1”]}}
    ,并且查询的组件要多得多。我无法手动将它们添加到一起。您可以动态创建整个管道。您可以首先处理输出对象字段并添加必要的查询运算符,然后根据输出字段调整聚合阶段并组装到聚合管道中。下面是一个这样的示例,我正在查看该示例,似乎我必须为查询的每个级别创建一个此类运算符。例如,我必须创建一个用于
    测试.tests.name
    ,另一个用于
    测试.tests.thing
    ,还有一个用于
    top.place
    。这准确吗?它确实取决于聚合查询。我提供的查询不需要用于
    测试的操作符,但需要用于匹配查询部分的操作符。只要你们同意一个固定的输出字段格式,你们就可以根据你们的查询进行调整。我已经更新了答案,加入了javascript代码来转换你们的查询和提供的答案的
    outputObject
    。这将使您了解如何处理动态查询。使用您的解决方案非常有效,除非
    顶部没有值。例如,当
    var outputObject={“testing”:{“tests”:{“name”:[“1”],“thing”:[“X”,“Y”]}         } };解决方案失败。无论
    顶部是否有内容,是否有办法使其正常工作?@black\u sheep07如果你真的阅读了内容,我告诉你两次,你的pr