我正在尝试根据日期值返回一个包含最大子文档的文档。到目前为止,我能够创建正确的对象,但是查询返回所有子文档,而不是具有最大日期的子文档。例如,我的数据存储为:
{ value: 1,
_id: 5cb9ea0c75c61525e0176f96,
name: 'Test',
category: 'Development',
subcategory: 'Programming Languages',
status: 'Supported',
description: 'Test',
change:
[ { version: 1,
who: 'ATL User',
when: 2019-04-19T15:30:39.912Z,
what: 'Item Creation' },
{ version: 2,
who: 'ATL Other User',
when: 2019-04-19T15:30:39.912Z,
what: 'Name Change' } ],
}
在我的查询中,我选择了所有具有相同subcategory
的项目,以及存在它们的name
。然后,我在对象中投影我需要的所有值,展开并对数组进行排序,并返回查询结果。结构方面,这让我在这里建模了正确的输出:
{
_id: 5cb9ea0c75c61525e0176f96,
name: 'Test',
category: 'Development',
subcategory: 'Programming Languages',
status: 'Supported',
description: 'Test',
change: {
"who": "ATL User",
"when": ISODate("2019-04-19T17:11:36Z")
}
}
这里的问题是,如果一个文档有多个子文档 - 或多个版本 - 那么查询也会返回这些子文档,而不是省略它们,只留下最大日期(如果项目Test
有三个版本,则返回三个Test
文档)。
我应该查看什么才能使用此查询否定其他文档?
db.items.aggregate([
{$match: {subcategory: "Programming Languages", name: {$exists: true}}},
{$project: {"name": 1,
"category": 1,
"subcategory": 1,
"status": 1,
"description": 1,
"change.who": 1,
"change.when": {$max: "$change.when"}}},
{$unwind: "$change"},
{$sort: {"change.when": -1}}
]);
首先,让我们以人们可以使用它并产生所需结果的方式显示您的数据:
{ value: 1,
_id: ObjectId('5cb9ea0c75c61525e0176f96'),
name: 'Test',
category: 'Development',
subcategory: 'Programming Languages',
status: 'Supported',
description: 'Test',
change:
[ { version: 1,
who: 'ATL User',
when: new Date('2019-04-19T15:30:39.912Z'),
what: 'Item Creation' },
{ version: 2,
who: 'ATL Other User',
when: new Date('2019-04-19T15:31:39.912Z'),
what: 'Name Change' } ],
}
请注意,"when"
日期实际上是不同的,因此会有一个$max
值,它们不仅仅是相同的。现在我们可以浏览这些案例了
案例 1 - 获取"单数"$max
值
这里的基本情况是使用$arrayElemAt
和$indexOfArray
运算符返回匹配的$max
值:
db.items.aggregate([
{ "$match": {
"subcategory": "Programming Languages", "name": { "$exists": true }
}},
{ "$addFields": {
"change": {
"$arrayElemAt": [
"$change",
{ "$indexOfArray": [
"$change.when",
{ "$max": "$change.when" }
]}
]
}
}}
])
返回:
{
"_id" : ObjectId("5cb9ea0c75c61525e0176f96"),
"value" : 1,
"name" : "Test",
"category" : "Development",
"subcategory" : "Programming Languages",
"status" : "Supported",
"description" : "Test",
"change" : {
"version" : 2,
"who" : "ATL Other User",
"when" : ISODate("2019-04-19T15:31:39.912Z"),
"what" : "Name Change"
}
}
基本上,"$max": "$change.when"
从该值数组中返回"最大值"的值。然后,您可以通过$indexOfArray
找到该值数组的匹配"索引",该数组返回找到的第一个匹配索引。然后,该"索引"位置(实际上只是以相同顺序转置的"when"
值数组)与$arrayElemAt
一起使用,以从指定索引位置的"change"
数组中提取"整个对象"。
案例 2 - 返回"多个"$max
条目
$max
几乎相同,只是这次我们$filter
返回与该$max
值匹配的多个"可能">值:
db.items.aggregate([
{ "$match": {
"subcategory": "Programming Languages", "name": { "$exists": true }
}},
{ "$addFields": {
"change": {
"$filter": {
"input": "$change",
"cond": {
"$eq": [ "$$this.when", { "$max": "$change.when" } ]
}
}
}
}}
])
返回:
{
"_id" : ObjectId("5cb9ea0c75c61525e0176f96"),
"value" : 1,
"name" : "Test",
"category" : "Development",
"subcategory" : "Programming Languages",
"status" : "Supported",
"description" : "Test",
"change" : [
{
"version" : 2,
"who" : "ATL Other User",
"when" : ISODate("2019-04-19T15:31:39.912Z"),
"what" : "Name Change"
}
]
}
所以$max
当然是相同的,但这次该运算符返回的奇异值用于$filter
内的$eq
比较。这将检查每个数组元素并查看当前"when"
值 ("$$this.when"
)。如果"等于">,则返回元素。
与第一种方法基本相同,但$filter
允许返回"多个">元素。因此,所有具有相同$max
值的东西。
情况 3 - 对数组内容进行预排序。
现在你可能会注意到,在我包含的示例数据中(改编自你自己的,但有一个实际的"max"日期),"max"值实际上是数组中的最后一个值。这可能是自然发生的,因为$push
(默认情况下)">追加">到现有数组内容的末尾。因此,"较新">的条目往往位于数组的末尾。
这当然是默认行为,但有充分的理由说明您"可能">想要更改它。简而言之,获取"最新">数组条目的最佳方法是从数组中返回第一个元素。
您实际需要做的就是确保"最新">实际上是首先添加而不是最后添加的。有两种方法:
使用
$position
"预置"数组项:这是一个简单的修饰符,用于$push
使用0
位置以始终添加到前面:db.items.updateOne( { "_id" : ObjectId("5cb9ea0c75c61525e0176f96") }, { "$push": { "change": { "$each": [{ "version": 3, "who": "ATL User", "when": new Date(), "what": "Another change" }], "$position": 0 } }} )
这会将文档更改为:
{ "_id" : ObjectId("5cb9ea0c75c61525e0176f96"), "value" : 1, "name" : "Test", "category" : "Development", "subcategory" : "Programming Languages", "status" : "Supported", "description" : "Test", "change" : [ { "version" : 3, "who" : "ATL User", "when" : ISODate("2019-04-20T02:40:30.024Z"), "what" : "Another change" }, { "version" : 1, "who" : "ATL User", "when" : ISODate("2019-04-19T15:30:39.912Z"), "what" : "Item Creation" }, { "version" : 2, "who" : "ATL Other User", "when" : ISODate("2019-04-19T15:31:39.912Z"), "what" : "Name Change" } ] }
请注意,这需要您事先实际"反转"所有数组元素,以便"最新"已经在前面,因此保持顺序。值得庆幸的是,这在第二种方法中有所涉及......
使用
$sort
按每个$push
的顺序修改文档:这是另一个修饰符,它实际上在每个新项目添加时以原子方式"重新排序"。正常用法与上述任何要$each
的新项目基本相同,甚至只是一个"空"数组,以便仅将$sort
应用于现有数据:db.items.updateOne( { "_id" : ObjectId("5cb9ea0c75c61525e0176f96") }, { "$push": { "change": { "$each": [], "$sort": { "when": -1 } } }} )
结果:
{ "_id" : ObjectId("5cb9ea0c75c61525e0176f96"), "value" : 1, "name" : "Test", "category" : "Development", "subcategory" : "Programming Languages", "status" : "Supported", "description" : "Test", "change" : [ { "version" : 3, "who" : "ATL User", "when" : ISODate("2019-04-20T02:40:30.024Z"), "what" : "Another change" }, { "version" : 2, "who" : "ATL Other User", "when" : ISODate("2019-04-19T15:31:39.912Z"), "what" : "Name Change" }, { "version" : 1, "who" : "ATL User", "when" : ISODate("2019-04-19T15:30:39.912Z"), "what" : "Item Creation" } ] }
可能需要一分钟来理解为什么要
$push
才能$sort
这样的数组,但一般目的是对数组进行修改,从而"更改"属性(如正在排序的Date
值),并且您将使用这样的语句来反映这些更改。或者实际上只是在$sort
添加新项目并让它发挥作用。
那么为什么要像这样"存储">排序的数组呢?如前所述,您希望第一项作为"最新">项,然后返回的查询只需变为:
db.items.find(
{
"subcategory": "Programming Languages",
"name": { "$exists": true }
},
{ "change": { "$slice": 1 } }
)
返回:
{
"_id" : ObjectId("5cb9ea0c75c61525e0176f96"),
"value" : 1,
"name" : "Test",
"category" : "Development",
"subcategory" : "Programming Languages",
"status" : "Supported",
"description" : "Test",
"change" : [
{
"version" : 3,
"who" : "ATL User",
"when" : ISODate("2019-04-20T02:40:30.024Z"),
"what" : "Another change"
}
]
}
因此,该$slice
可以仅用于通过已知索引提取数组项。从技术上讲,您可以在那里使用-1
来返回数组的最后一项,但是在最近的重新排序中,首先允许其他事情,例如确认上次修改是由某个用户进行的,和/或其他条件,如日期范围约束。即:
db.items.find(
{
"subcategory": "Programming Languages",
"name": { "$exists": true },
"change.0.who": "ATL User",
"change.0.when": { "$gt": new Date("2018-04-01") }
},
{ "change": { "$slice": 1 } }
)
这里要注意的是,像"change.-1.when"
这样的东西是一个非法的声明,这基本上就是我们重新排序数组的原因,以便您可以将法律0
用于第一个而不是-1
用于最后一个。
结论
因此,您可以执行几种不同的操作,方法是使用聚合方法来筛选数组内容,或者在对数据的实际存储方式进行一些修改后通过标准查询表单。使用哪一个取决于您自己的情况,但应该注意的是,任何标准查询表单的运行速度都比通过聚合框架或任何计算运算符进行的任何操作都要快得多。