从Array获取最新的子文档

时间:2017-07-02 23:45:28

标签: node.js mongodb mongoose mongodb-query aggregation-framework

我有一个数组。 我想从revision 数组(复数)中选择编号history最高的对象。

我的文档看起来像这样(通常它不仅仅是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"
        }
    ]
}

我选择文档的代码:

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)
        })
    })
}

如何选择文档以便它只从历史数组中输出具有最高版本号的对象?

1 个答案:

答案 0 :(得分:1)

您可以通过几种不同的方式解决这个问题。它们的方法和性能当然各不相同,我认为您需要对设计做出一些更大的考虑。最值得一提的是"需要"为"修订"实际应用程序的使用模式中的数据。

通过聚合查询

至于从内部数组"获取"最后一个元素的最重要的一点,那么你真的应该使用.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()等常规猫鼬方法不同,$match实际上需要"施放"如果输入值实际上是"字符串"则为ObjectId。猫鼬不能应用"架构类型"在聚合管道中,您可能需要自己执行此操作,尤其是projectId来自请求参数时:

  { "$match": { "project_id": Schema.Types.ObjectId(projectId) } },

这里的基本部分是我们使用$map迭代所有"uploaded_files"条目,然后简单地提取"最新的"来自"history"数组$arrayElemAt使用" last" index,-1

这应该是合理的,因为它最有可能是最近的修订版"实际上是"最后"数组输入。我们可以通过将$max作为$filter的条件来调整此选项来寻找最大的"。因此管道阶段变为:

     { "$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值进行比较之外,这或多或少是相同的,并且只从数组中返回"一个" 条目索引从"过滤"排列"第一个"位置,或0索引。

关于使用$lookup代替.populate()的其他常规技巧,请参阅"Querying after populate in Mongoose"上的条目,其中介绍了采用此方法时可以优化的内容。< / p>

通过填充

查询

当然,我们也可以使用.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)

你当然在哪里回来&#34;所有&#34;来自"history"的项目,但我们只需应用.map()来调用这些元素上的.slice(),以便再次获取每个元素的最后一个数组元素。

由于返回了所有历史记录而导致开销增加,.populate()调用是其他请求,但它确实获得了相同的最终结果。

设计点

我在这里看到的主要问题是你甚至有一个&#34;历史&#34;内容中的数组。这不是一个好主意,因为您需要执行上述操作才能返回您想要的相关项目。

所以作为设计点&#34;,我不会这样做。但相反,我会&#34;分开&#34;从所有情况下的项目的历史。保持&#34;嵌入&#34;文件,我会保留&#34;历史&#34;在一个单独的数组中,只保留&#34;最新的&#34;修订与实际内容:

{
    "_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相关条目并在&#34;历史记录&#34;上使用$push来维护此问题。在一次操作中:

.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
      }
    }
  }
)

将数组分开,然后您可以简单地查询并始终得到最新的,并丢弃&#34;历史记录&#34;直到你真正想要提出这个要求为止:

Project.findOne({ "project_id": projectId })
  .select('-file_history')      // The '-' here removes the field from results
  .populate(populateQuery)

作为一般情况,虽然我根本不打扰&#34;修订版&#34;完全没有数字。当&#34;追加&#34;保持大部分相同的结构时,你真的不需要它。自#&#34;最新&#34;以来的一个数组总是&#34;最后&#34;。对于改变结构也是如此,其中最新的&#34;最新的&#34;将永远是给定上传文件的最后一个条目。

努力维持这样一个人造的&#34;索引充满了问题,并且大多数都破坏了原子的变化。此处.update()示例中显示的操作,因为您需要知道&#34;计数器&#34;值,以提供最新的修订号,因此需要&#34;读&#34;来自某个地方。