从数组获取最新的子文档

我有一个数组。 我想从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”值来提供最新版本号,因此需要从某个地方“读”。