聚合
使用聚合框架,您可以将$indexOfArray 应用到可用的位置:
Model.aggregate([
{ "$addFields": {
"difference": {
"$subtract": [
{ "$cond": [
{ "$eq": [{ "$indexOfArray": ["$experiences.to", null] }, -1] },
{ "$max": "$experiences.to" },
new Date()
]},
{ "$min": "$experiences.from" }
]
}
}}
])
如果“最新”始终是数组中的最后一个,则使用$arrayElemAt:
Model.aggregate([
{ "$addFields": {
"difference": {
"$subtract": [
{ "$cond": [
{ "$eq": [{ "$arrayElemAt": ["$experiences.to", -1] }, null] },
new Date(),
{ "$max": "$experiences.to" }
]},
{ "$min": "$experiences.from" }
]
}
}}
])
这几乎是最有效的方法,作为应用$min 和$max 运算符的单个管道阶段。对于$indexOfArray,您至少需要 MongoDB 3.4,而对于简单地使用 $arrayElemAt,您可以使用 MongoDB 3.2,这是您应该在生产环境中运行的最低版本。
一次通过,意味着它可以以很少的开销快速完成。
简短的部分是$min 和$max 允许您直接从数组元素中提取适当的值,即数组中"from" 的“最小”值和"to" 的最大值。在可用的情况下,$indexOfArray 运算符可以从提供的数组(在本例中为 "to" 值)返回匹配的索引,其中存在指定的值(如null 此处)。如果存在则返回该值的索引,如果不存在则返回 -1 的值,表示未找到。
我们使用$cond 这是一个“三元”或if..then..else 运算符来确定当找不到null then 时,您需要来自"to" 的$max 值。当然,当它被发现是 else 时,会返回当前 Date 的值,该值作为执行时的外部参数输入聚合管道。
MongoDB 3.2 的另一种情况是,您改为“假定”数组的最后一个元素是最近的就业历史项目。通常最好的做法是订购这些项目,所以最近的是“最后一个”(如您的问题中所示)或数组的“第一个”条目。保持这些条目的顺序是合乎逻辑的,而不是依赖于在运行时对列表进行排序。
所以当使用“已知”位置如“last”时,我们可以使用$arrayElemAt 运算符从数组中返回指定位置的值。这里是“最后一个”元素的-1。 “第一个”元素将是 0,并且可以说也可以应用于获取 "from" 的“最小”值,因为您应该按顺序排列数组。同样$cond 用于根据是否返回null 来转置值。作为$max 的替代品,您甚至可以使用$ifNull 来交换值:
Model.aggregate([
{ "$addFields": {
"difference": {
"$subtract": [
{ "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
{ "$min": "$experiences.from" }
]
}
}}
])
如果第一个条件的响应是null,则该运算符实质上会切换返回的值。因此,由于我们已经从“最后一个”元素中获取值,我们可以“假设”这确实意味着 "to" 的“最大”值。
$subtract 是实际返回“差异”的原因,因为当您从另一个日期“减去”一个日期时,差异将作为两者之间的毫秒值返回。这就是 BSON 日期在内部实际存储的方式,它是日期格式的常见内部日期存储,即“自纪元以来的毫秒数”。
如果您想要特定持续时间的间隔,例如“年”,那么应用“日期数学”来更改日期值之间的 毫秒 差异是一件简单的事情。因此,通过从间隔中除以进行调整(为了完整起见,还在"from" 上显示$arrayElemAt):
Model.aggregate([
{ "$addFields": {
"difference": {
"$floor": {
"$divide": [
{ "$subtract": [
{ "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
{ "$arrayElemAt": ["$experiences.from", 0] }
]},
1000 * 60 * 60 * 24 * 365
]
}
}
}}
])
这使用$divide 作为数学运算符,1000 毫秒60 用于秒和分钟,24 小时和365 天作为除数值。 $floor 从小数位“向下舍入”数字。你可以在那里做任何你想做的事情,但它“应该”“内联”使用,而不是在单独的阶段,这只会增加处理开销。
当然,365 天的假设充其量只是一个“近似值”。如果您想要更完整的内容,则可以改为将date aggregation operators 应用于值以获得更准确的读数。所以在这里,也应用$let 声明为“变量”以供以后操作:
Model.aggregate([
{ "$addFields": {
"difference": {
"$let": {
"vars": {
"to": { "$ifNull": [{ "$arrayElemAt": ["$experiences.to", -1] }, new Date()] },
"from": { "$arrayElemAt": ["$experiences.from", 0] }
},
"in": {
"years": {
"$subtract": [
{ "$subtract": [
{ "$year": "$$to" },
{ "$year": "$$from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
"then": 0,
"else": 1
}}
]
},
"months": {
"$add": [
{ "$subtract": [
{ "$month": "$$to" },
{ "$month": "$$from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
"then": 0,
"else": 12
}}
]
},
"days": {
"$add": [
{ "$subtract": [
{ "$dayOfYear": "$$to" },
{ "$dayOfYear": "$$from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$to" },{ "$month": "$$from" }] },
"then": 0,
"else": 365
}}
]
}
}
}
}
}}
])
这又是一年中日期的轻微近似值。 MongoDB 3.6 实际上允许您通过实现 $dateFromParts 来测试“闰年”,以确定 2 月 29 日在当年是否有效,方法是从我们可用的“部分”组装。
使用返回的数据
当然,以上所有内容都是使用聚合框架来确定每个人的数组的间隔。如果您打算通过根本不返回数组项来“减少”返回的数据,或者如果您希望这些数字进一步聚合以报告更大的“总和”或“平均”统计数据,这将是建议的课程数据。
另一方面,如果您确实希望为该人返回所有数据,包括完整的“经验”数组,那么最好的做法可能是在从服务器在您处理每个返回的项目时。
此方法的简单应用是将新字段“合并”到结果中,就像 $addFields 所做的那样,但在“客户端”端:
Model.find().lean().cursor().map( doc =>
Object.assign(doc, {
"difference":
((doc.experiences.map(e => e.to).indexOf(null) === -1)
? Math.max.apply(null, doc.experiences.map(e => e.to))
: new Date() )
- Math.min.apply(null, doc.experiences.map(e => e.from)
})
).toArray((err, result) => {
// do something with result
})
这只是将第一个聚合示例中表示的相同逻辑应用于结果游标的“客户端”处理。由于您使用的是 mongoose,.cursor() 方法实际上从底层驱动程序返回了一个 Cursor 对象,为了“方便”,mongoose 通常会将其隐藏起来。在这里我们想要它,因为它让我们可以访问一些方便的方法。
Cursor.map() 是一种方便的方法,它允许对从服务器返回的内容应用“转换”。这里我们使用Object.assign() 将新属性“合并”到返回的文档中。我们可以交替使用 Array.map() 对 mongoose “默认”返回的“数组”,但内联处理看起来更简洁,也更高效。
事实上Array.map() 是这里操作的主要工具,因为我们在聚合语句中应用了"$experiences.to" 之类的语句,我们使用doc.experiences.map(e => e.to) 应用在“客户端”上,它执行相同的操作“转换”将对象数组转换为指定字段的“值数组”。
这允许使用Array.indexOf() 对值数组进行相同的检查,并且Math.min() 和Math.max() 也以相同的方式使用,实现apply() 以使用这些“映射”数组值作为参数函数的值。
当然,由于我们仍然返回了 Cursor,因此我们将其转换回更典型的形式,您可以使用 Cursor.toArray() 将 mongoose 结果作为“数组”处理,这正是 mongoose 在“下”所做的引擎盖”为您提供默认请求。
Query.lean() 是一个 mongoose 修饰符,它基本上表示返回并期望“普通 JavaScript 对象”,而不是与模式匹配的“mongoose 文档”,应用方法又是默认返回。我们想要这样,因为我们正在“操纵”结果。另一种方法是在“返回”默认数组“之后”进行操作,并通过所有 mongoose 文档中存在的.toObject() 进行转换,以防“序列化虚拟属性”对您很重要。
所以这本质上是第一种聚合方法的“镜像”,但适用于“客户端”逻辑。如前所述,当您实际上希望文档中的所有属性无论如何都包含在结果中时,这样做通常更有意义。原因很简单,因为在从服务器返回结果“之前”返回的结果中添加“附加”数据是没有意义的。因此,只需在数据库返回它们“之后”应用转换即可。
与上述非常相似,可以应用与所有聚合示例中演示的相同的客户端转换方法。您甚至可以使用外部库进行日期操作,这些库为您提供了一些“原始数学”方法的“帮助”。