如前所述,您的真正问题与数据的结构有关。虽然通过命名键组织和访问事物通常被宣传为客户端代码中数据访问的最佳模式,但确切的相反通常适用于数据库,而 MongoDB也不例外。
数据库基本上希望 values 执行搜索而不是 keys,因此您实际上需要通过运行时强制更改所有数据em>keys 到 values 中,以便按照您想要的方式对其进行过滤。
也就是说,这里有一个显示方法的清单:
const { MongoClient } = require('mongodb');
const url = 'mongodb://localhost:27017';
const opts = { useNewUrlParser: true, useUnifiedTopology: true };
// Basic logging helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// Sample document
const data = {
data: {
col1: {
'12-07-2012': 'value1',
'13-07-2012': 'value2',
'14-07-2012': 'value3',
'15-07-2012': 'value5'
},
col2: {
'12-07-2012': 'value1',
'13-07-2012': 'value2',
'14-07-2012': 'value3',
'15-07-2012': 'value5'
},
col3: {
'12-07-2012': 'value1',
'13-07-2012': 'value2',
'14-07-2012': 'value3',
'15-07-2012': 'value5'
}
}
};
// Sample input conditions
const input = {
col1: {
'12-07-2012': 'value1', // clearly pairs of "from" and "to"
'14-07-2012': 'value3'
},
col2: {
'12-07-2012': 'value1',
'14-07-2012': 'value3'
},
col3: {
'12-07-2012': 'value1',
'14-07-2012': 'value3'
}
};
// Helper for converting strings to valid ISO dates
const toDate = dateStr => new Date(dateStr.split("-").reverse().join("-"));
// Helper for the $filter arguments for $or
const makeCond = input => Object.entries(input)
// get key and value pairs of object and make an array per 'key'
.map(([k,v]) =>
({
// Reduce the v objects as key value pairs into a single array
'$and': Object.entries(v).reduce((o, [k,v], i) =>
[
...o, // spread the reduced array
// Add and spread these new array elements
...[
// Use $gte or $lte depending on current index
{ [(i == 0) ? '$gte' : '$lte']: [ '$$this.date', toDate(k) ] },
{ [(i == 0) ? '$gte' : '$lte']: [ '$$this.value', v ] }
]
],
// The initial array for reduce
[{ '$eq': [ '$$this.col', k ] }])
})
);
const makeOrCondition = input => Object.entries(input)
.map(([col,v]) =>
({
col,
date: Object.keys(v).reduce((o,k,i) =>
({ ...o, [(i == 0) ? '$gte' : '$lte']: toDate(k) }), {}),
value: Object.values(v).reduce((o,v,i) =>
({ ...o, [(i == 0) ? '$gte': '$lte']: v }), {})
})
);
(async function() {
let client;
try {
client = await MongoClient.connect(url, opts);
let db = client.db('test');
await db.collection('example').deleteMany({});
await db.collection('example').insertOne(data);
// Debug the makeCond
//log(makeCond(input));
// Covert objects to arrays of arrays
const mapObjects = {
'$map': {
'input': { '$objectToArray': '$data' },
'in': {
'$let': {
'vars': { 'col': '$$this.k' },
'in': {
'$map': {
'input': { '$objectToArray': '$$this.v' },
'in': {
'col': '$$col',
'date': { '$toDate': '$$this.k' },
'value': '$$this.v'
}
}
}
}
}
}
};
// Flatten arrays of arrays to single array
const joinArrays = {
'$reduce': {
'input': mapObjects,
'initialValue': [],
'in': { '$concatArrays': [ '$$value', '$$this' ] }
}
};
// Apply the filter to the array elements
const filterArray = {
'$filter': {
'input': joinArrays,
'cond': { '$or': makeCond(input) }
}
};
// Basically an inline version of $group
const grouper = {
'$reduce': {
'input': filterArray,
'initialValue': [],
'in': {
'$let': {
'vars': { 'current': '$$this' },
'in': {
'$concatArrays': [
// Filter reduce output from the matching col
{ '$filter': {
'input': '$$value',
'cond': { '$ne': [ '$$current.col', '$$this.k' ] }
}},
// Conditionally join to:
{ '$cond': {
'if': {
'$ne': [
{ '$indexOfArray': [
'$$value.k', '$$this.col'
]},
-1
]
},
// Concat the inner array where matched
'then': [{
'k': '$$this.col',
'v': {
'$concatArrays': [
{ '$arrayElemAt': [
'$$value.v',
{ '$indexOfArray': ['$$value.k', '$$this.col'] }
]},
[{ 'k': '$$this.date', 'v': '$$this.value' }]
]
}
}],
// Create the inner array where not matched
'else': [{
'k': '$$this.col',
'v': [{
'k': '$$this.date',
'v': '$$this.value'
}]
}]
}}
]
}
}
}
}
};
const pipeline = [
{ '$match': {
'$expr': { '$gt': [{ '$size': filterArray }, 0] }
}},
{ '$project': {
'data': {
'$arrayToObject': {
'$map': {
'input': grouper,
'in': {
// reformat
'k': '$$this.k',
'v': {
'$arrayToObject': {
'$map': {
'input': '$$this.v',
'in': {
'k': {
'$dateToString': {
'date': '$$this.k',
'format': '%d-%m-%Y'
}
},
'v': '$$this.v'
}
}
}
}
}
}
}
}
}}
];
log(pipeline);
let result = await db.collection('example').aggregate(pipeline).toArray();
log(result);
// Create example2
await db.collection('example').aggregate([
{ '$project': { 'data': joinArrays } },
{ '$out': 'example2' }
]).toArray();
/*
* Simple $elemMatch and $filter usage when already an array
*
*/
let result2 = await db.collection('example2').aggregate([
{ '$match': {
'data': {
'$elemMatch': {
'$or': makeOrCondition(input)
}
}
}},
{ '$project': {
'data': {
'$filter': {
'input': '$data',
'cond': { '$or': makeCond(input) }
}
}
}}
]).toArray();
log(result2);
// Create example3
await db.collection('example2').aggregate([
{ '$unwind': '$data' },
{ '$replaceRoot': { 'newRoot': '$data' } },
{ '$out': 'example3' }
]).toArray();
/*
* Really simple when the elements are discreet documents
* in their own collection
*/
let result3 = await db.collection('example3').find({
'$or': makeOrCondition(input)
}).toArray();
log(result3);
} catch (e) {
console.error(e);
} finally {
if (client)
client.close();
}
})()
示例 1
这是首先执行的主要aggregate() 列表,基本上就是您所要求的。你会看到这个输出产生了不满足input文档中提供的条件的键的期望删除:
{
"_id": "5d6a7ac8736dce1c76d9d3e8",
"data": {
"col1": {
"12-07-2012": "value1",
"13-07-2012": "value2",
"14-07-2012": "value3"
},
"col2": {
"12-07-2012": "value1",
"13-07-2012": "value2",
"14-07-2012": "value3"
},
"col3": {
"12-07-2012": "value1",
"13-07-2012": "value2",
"14-07-2012": "value3"
}
}
}
基本上完成的方法是使用$filter 运算符从数组 中删除不满足条件的元素。但为了做到这一点,您需要应用 $objectToArray 以便将 keys 转换为具有 k 和 v 属性的对象,其中包含 values和每个属性的价值。注意部分:
// Covert objects to arrays of arrays
const mapObjects = {
'$map': {
'input': { '$objectToArray': '$data' },
'in': {
'$let': {
'vars': { 'col': '$$this.k' },
'in': {
'$map': {
'input': { '$objectToArray': '$$this.v' },
'in': {
'col': '$$col',
'date': { '$toDate': '$$this.k' },
'value': '$$this.v'
}
}
}
}
}
}
};
它也使用$map 来处理元素并将内部 对象映射 到k 和v 属性的数组中。还要注意$toDate,它足够聪明,可以识别字符串的dd-mm-yyy 格式并转换为BSON 日期进行比较。
另外需要注意的是$reduce 的用法,以便展平由嵌套结构(显示为joinArrays)生成的数组数组 和实际$filter 条件:
// Apply the filter to the array elements
const filterArray = {
'$filter': {
'input': joinArrays,
'cond': { '$or': makeCond(input) }
}
};
这里的makeCond() 实际上是为了将问题中的input 样本(为了匹配提供的数据而更正)转换为实际表达式,以便在cond 的参数中使用@987654327 @。您可以在程序输出中查看生成的管道,看看它的实际外观,但那是执行实际过滤的部分。
您还可以注意到,此处使用的实际 pipeline 只需要两个管道阶段,$match 用于仅选择返回仍然匹配这些条件的键的文档,以及执行实际工作的 $project在返回结果之前从文档中删除不满足条件的键。
另请注意,$map 和 $reduce 的其他部分嵌套在 $filter 表达式中,当然,整个表达式在两个管道阶段都得到重用。
在实际的$project 中,我们以不同的方式使用$reduce,以便将数据分组重新组合在一起,以准备好与原始文档一样的预期输出形式。这可以交替使用单独的 $unwind 和 $group 阶段来完成,但这样做远没有那么有效,即使它可能更容易阅读。
其他需要注意的是$indexOfArray 和$arrayElemAt 的用法,它们有助于分组全部在$cond 中处理if/then/else 逻辑。这是另一个带有内部数组连接的缩减数组,因此这里也使用了$concatArrays。
最后,为了返回带有命名键的原始对象形式,BSON 日期值需要转换为字符串才能对键名有效。 $dateToString 运算符接受带有"%d-%m-%Y" 格式字符串的format 参数恢复为原始格式。
可能需要一段时间才能深入了解,但这些是链接,代码中有 cmets。会有更多解释,但 Stack Overflow 只允许有限的答案空间,这接近了这个限制。阅读并运行示例代码以了解详细信息以及参考主要方法的引用链接。
示例 2
代码基本上是为了表明示例中提供的主要“提升” 以及对问题的直接回答基本上都是关于将文档内容转换为数组,因此“日期”和其他条件可以从内容中过滤出来。
“示例 2”代码的重点是表明,当您将 data 属性构造为一个 数组 开始时,查询操作会变得更加简单和高效。
当您打算进行过滤或任何其他查询操作而是在 values 上而不是在 keys 上,就像在开场白中提到的那样。
示例 3
这基本上是为了证明,如果您唯一关心的是在文档中处理 data 的内容,那么实际上将这些条目作为谨慎的文档分离到它们自己的集合中,这将是最简单的查询形式,并且迄今为止最有效的,因为根本不需要计算任何东西,并且可以在整个过程中使用索引。
作为谨慎的文档,该过程实际上是仅查询,根本不需要aggregate() 处理。这使它快速。
结论
虽然大多数事情可能使用聚合框架,但它仍然不是总是推荐的解决方案。这也应该证明设计在考虑如何实际使用您的数据时的重要性。
因此,简而言之,如果您想以一种有意义的方式“查询”任何内容,而又不会引入不必要的开销(这会破坏应用程序性能并大大增加代码维护的复杂性),那么请使用 values 而不是keys 来识别那些有意义的数据点,而不是以这种方式使用。