2017-07-07 33 views
2

我試圖動態查詢,看起來像這樣一個數據庫:動態查詢從輸入對象

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. 我需要把它應用到$match語句之前修改的對象outputObject
  2. 我的查詢是否正確?
  3. 我是否應該使用$and$in結合$match聲明?
  4. 哪些代碼會產生所需的結果?

目前使用mongoDB 3.4.4

回答

2

你有幾個問題在這裏。首先,輸入值中的數組參數應該與$in進行比較,其中許多「列表中的任何一個」爲了匹配。

的第二個問題是,由於路徑是「嵌套」在這裏,你確實需要,否則你的第一個問題的另一種變體,其中的條件要尋找的"test"數組中的元素轉換爲"dot notation"只有具有您在輸入中指定的提供的字段。

所以,除非你「點擊」路徑以及由於你的數組項也包含"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。所以通常以這種方式獲得「獨特」更好。

+0

使用你的解決方案工作得很好,除非在頂部沒有值。例如,當'VAR outputObject = { 「測試」:{ 「測試」:{ 「名稱」:[ 「1」 ], 「東西」:[ 「X」, 「Y」 ] } } };'解決方案失敗。有沒有辦法使它的功能不管在頂部是否有東西? –

+0

@ black_sheep07如果您實際閱讀內容,我會告訴您兩次,您需要單獨的「頂部」和「底部」部分的推定是不正確的,事實上,您需要條件才能應用最初和以後的過濾。我還記得以前跟你說過的話[當你對問題有不同的問題時,你可以單獨提問](https://stackoverflow.com/questions/ask)。 –

+1

@ black_sheep07換句話說,這是一個「代碼中的單個語句」Object.assign(outputObject.top,outputObject.testing)'這是唯一引用我說你應該「擺脫」的兩個鍵因爲沒有必要讓他們在那裏。事實上,這個陳述所做的是通過將來自兩個鍵的結果「合併」爲單個文檔來「移除」這些鍵。 –

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" 
     } 
    } 
    } 
] 
+0

不幸的是,該對象是不固定的。有時,這將是'無功outputObject = { 「頂」:{ 「地方」: 「A」 ] }, 「測試」:{ 「測試」:{ 「名」:[ 「1」 ] } } }' 並且查詢中有顯着更多的組件。手動添加它們不是我能做的事情。 –

+0

您可以動態創建整個管道。您可以先處理輸出對象字段並添加必要的查詢運算符,然後根據輸出字段您可以調整聚合階段並將其組合成聚合管道。下面是一個這樣的示例https://stackoverflow.com/questions/43889978/mongoose如何寫一個查詢與條件/ 43959932#43959932 – Veeram

+0

我在看例子,似乎我將不得不爲每個查詢級別創建一個這些運算符。例如,我必須爲'tests.tests.name'創建一個,另一個用於'testing.tests.thing',另一個用於'top.place'。這是否準確? –