从数组获取最新的子文档
我有一个数组。 我想从history
数组 (复数)中selectrevision
号最高的对象。
我的文档看起来像这样(通常它不仅仅是uploaded_files
一个对象):
{ "_id" : ObjectId("5935a41f12f3fac949a5f925"), "project_id" : 13, "updated_at" : ISODate("2017-07-02T22:11:43.426Z"), "created_at" : ISODate("2017-06-05T18:34:07.150Z"), "owner" : ObjectId("591eea4439e1ce33b47e73c3"), "name" : "Demo project", "uploaded_files" : [ { "history" : [ { "file" : ObjectId("59596f9fb6c89a031019bcae"), "revision" : 0 } ], "_id" : ObjectId("59596f9fb6c89a031019bcaf") "display_name" : "Example filename.txt" } ] }
我select文件的代码:
function getProject(req, projectId) { let populateQuery = [ {path: 'owner'}, {path: 'uploaded_files.history.file'} ] return new Promise(function (resolve, reject) { Project.findOne({ project_id: projectId }).populate(populateQuery).then((project) => { if (!project) reject(new createError.NotFound(req.path)) resolve(project) }).catch(function (err) { reject(err) }) }) }
我怎样才能select文件,使其只输出历史数组中最高版本号的对象?
你可以用几种不同的方法解决这个问题。 当然,它们的方法和性能各不相同,我认为您需要对devise进行一些更大的考虑。 最值得注意的是这里是您的实际应用程序的使用模式中的“修订”数据的“需要”。
通过聚合查询
至于从“内部数组中获取最后一个元素”的最重要的一点,那么你真的应该使用.aggregate()
操作来做到这一点:
function getProject(req,projectId) { return new Promise((resolve,reject) => { Project.aggregate([ { "$match": { "project_id": projectId } }, { "$addFields": { "uploaded_files": { "$map": { "input": "$uploaded_files", "as": "f", "in": { "latest": { "$arrayElemAt": [ "$$f.history", -1 ] }, "_id": "$$f._id", "display_name": "$$f.display_name" } } } }}, { "$lookup": { "from": "owner_collection", "localField": "owner", "foreignField": "_id", "as": "owner" }}, { "$unwind": "$uploaded_files" }, { "$lookup": { "from": "files_collection", "localField": "uploaded_files.latest.file", "foreignField": "_id", "as": "uploaded_files.latest.file" }}, { "$group": { "_id": "$_id", "project_id": { "$first": "$project_id" }, "updated_at": { "$first": "$updated_at" }, "created_at": { "$first": "$created_at" }, "owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } }, "name": { "$first": "$name" }, "uploaded_files": { "$push": { "latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] }, "_id": "$$uploaded_files._id", "display_name": "$$uploaded_files.display_name" } } }} ]) .then(result => { if (result.length === 0) reject(new createError.NotFound(req.path)); resolve(result[0]) }) .catch(reject) }) }
由于这是一个聚合语句,我们也可以在“服务器”上执行“连接”,而不是通过使用$lookup
来提出额外的请求(这就是.populate()
实际上在这里做的事情),所以我可以自由一些因为您的模式不包含在问题中,所以实际的集合名称。 没关系,因为你没有意识到你可以这样做。
当然,“实际的”集合名称是服务器所要求的,它没有“应用程序”定义模式的概念。 有些事情你可以为了方便而做,但是稍后会更多。
你还应该注意,根据projectId
实际来自哪里,然后不像像.find()
这样的常规mongoose方法,如果input值实际上是一个“string”,则$match
实际上需要“投射”到ObjectId
。 Mongoose不能在一个聚合pipe道中应用“模式types”,所以你可能需要自己做,特别是如果projectId
来自一个请求参数:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
这里的基本部分是我们使用$map
遍历所有"uploaded_files"
条目的地方,然后使用“last”索引(即-1
从$arrayElemAt
的"history"
数组中提取“latest”。
这应该是合理的,因为最有可能的是“最近修订”实际上是“最后一个”数组条目。 我们可以通过将$max
作为$filter
的条件来适应这个寻找“最大的”。 所以这个pipe道阶段变成:
{ "$addFields": { "uploaded_files": { "$map": { "input": "$uploaded_files", "as": "f", "in": { "latest": { "$arrayElemAt": [ { "$filter": { "input": "$$f.history.revision", "as": "h", "cond": { "$eq": [ "$$h", { "$max": "$$f.history.revision" } ] } }}, 0 ] }, "_id": "$$f._id", "display_name": "$$f.display_name" } } } }},
除了我们与$max
值进行比较之外,它们或多或less是相同的,只返回数组中的“one”条目,使索引从“filtered”数组返回“first”位置,或者0
索引。
关于使用$lookup
代替.populate()
其他一般技巧,请参阅我在“在Mongoose中填充后查询”中的条目, .populate()
更多地讨论了采用这种方法时可以优化的东西。
通过填充查询
当然,我们也可以使用.populate()
调用和操作结果数组来完成(即使效率不高):
Project.findOne({ "project_id": projectId }) .populate(populateQuery) .lean() .then(project => { if (project === null) reject(new createError.NotFound(req.path)); project.uploaded_files = project.uploaded_files.map( f => ({ latest: f.history.slice(-1)[0], _id: f._id, display_name: f.display_name })); resolve(project); }) .catch(reject)
当然,你实际上是从"history"
返回“全部”项目,但是我们只需要应用一个.map()
来调用这些元素的.slice()
以获得每个元素的最后一个数组元素。
因为所有的历史logging都被返回,所以开销稍微增加一些,而.populate()
调用是额外的请求,但是它得到了相同的最终结果。
一个devise点
我在这里看到的主要问题是,你甚至在内容中有一个“历史”数组。 这不是一个好主意,因为你需要像上面这样做,以便只返回你想要的相关项目。
所以作为一个“devise点”,我不会这样做。 但相反,我会在所有情况下将历史与“项目”分开。 保持“embedded”的文件,我会保持“历史”在一个单独的数组,并只保留“最新”的修订与实际内容:
{ "_id" : ObjectId("5935a41f12f3fac949a5f925"), "project_id" : 13, "updated_at" : ISODate("2017-07-02T22:11:43.426Z"), "created_at" : ISODate("2017-06-05T18:34:07.150Z"), "owner" : ObjectId("591eea4439e1ce33b47e73c3"), "name" : "Demo project", "uploaded_files" : [ { "latest" : { { "file" : ObjectId("59596f9fb6c89a031019bcae"), "revision" : 1 } }, "_id" : ObjectId("59596f9fb6c89a031019bcaf"), "display_name" : "Example filename.txt" } ] "file_history": [ { "_id": ObjectId("59596f9fb6c89a031019bcaf"), "file": ObjectId("59596f9fb6c89a031019bcae"), "revision": 0 }, { "_id": ObjectId("59596f9fb6c89a031019bcaf"), "file": ObjectId("59596f9fb6c89a031019bcae"), "revision": 1 } }
您可以简单地通过设置$set
相关条目,并在一个操作中使用$push
对“history”进行维护:
.update( { "project_id": projectId, "uploaded_files._id": fileId } { "$set": { "uploaded_files.$.latest": { "file": revisionId, "revision": revisionNum } }, "$push": { "file_history": { "_id": fileId, "file": revisionId, "revision": revisionNum } } } )
在数组分开的情况下,您可以简单地查询并始终获取最新的数据,并放弃“历史logging”,直到您真正要发出该请求为止:
Project.findOne({ "project_id": projectId }) .select('-file_history') // The '-' here removes the field from results .populate(populateQuery)
尽pipe如此,我一般不会打扰“修改”号码。 由于“最新”始终是“最后一个”,因此保留大部分相同的结构时,并不需要“追加”数组。 改变结构也是如此,“最新”将始终是给定上传文件的最后一个条目。
试图维护这样一个“人为的”索引是充满了问题的,并且大部分情况下会破坏“primefaces”操作的任何变化,如这里的.update()
例子所示,因为你需要知道一个“counter”值来提供最新版本号,因此需要从某个地方“读”。