【问题标题】:Return Document with Max Sub Document返回具有最大子文档的文档
【发布时间】:2019-04-20 11:12:59
【问题描述】:

我正在尝试根据日期值返回包含最大子文档的文档。到目前为止,我能够创建正确的对象,但是查询返回的是所有子文档,而不是具有最大日期的子文档。例如,我的数据存储为:

{ 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 有三个版本,那么三个 @ 987654326@ 文档被退回)。

为了用这个查询否定那些其他文档,我应该看什么?

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

【问题讨论】:

    标签: node.js mongodb


    【解决方案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 找到该值数组的匹配“索引”,它返回找到的 first 匹配索引。该“索引”位置(实际上只是一个以相同顺序转置的"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 比较。这将检查每个数组元素并查看 current "when" 值 ("$$this.when")。其中 "equal" 则返回元素。

    与第一种方法基本相同,但$filter 允许返回“多个” 元素。因此 everything 具有 same $max 值。

    案例 3 - 对数组内容进行预排序。

    现在您可能会注意到,在我包含的示例数据中(改编自您自己的但具有实际“最大”日期),“最大”值实际上是数组中的 last 值。这可能会自然而然地发生,因为$push(默认情况下)“追加” 到现有数组内容的末尾。所以 "newer" 条目将倾向于位于数组的 end

    这当然是 默认 行为,但您有充分的理由“可能” 想要改变它。简而言之,获取“最近的”数组条目的最佳方法实际上是从数组中返回第一个元素

    您真正需要做的就是确保“最近的”实际上是首先而不是最后添加的。有两种方法:

    1. 使用$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"
                  }
          ]
      }
      

    请注意,这将需要您实际去“反转”所有数组元素,以便“最新”已经在前面,以便保持顺序。值得庆幸的是,这在第二种方法中有所涵盖...

    1. 使用$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 这样的数组,但总体意图是当可能对数组进行修改以“改变”像 @987654388 这样的属性时@value 被排序,您将使用这样的语句来反映这些更改。或者实际上只是使用$sort 添加新项目并让它发挥作用。

    那么为什么"store" 数组是这样排列的呢?如前所述,您希望第一个项作为“最近的”,然后返回的查询简单地变成:

    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 以返回数组的 last 项,但是最近的重新排序首先允许其他事情,例如确认最后一次修改是由某些用户和/或其他条件,例如日期范围限制。即:

    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" 之类的语句是非法语句,这就是我们重新排列数组的原因,以便您可以使用 legal 0 代替 first -1最后

    结论

    因此,您可以执行多种不同的操作,或者通过使用聚合方法过滤数组内容,或者在对数据的实际存储方式进行一些修改后通过标准查询表单。使用哪一个取决于您自己的情况,但应注意,任何标准查询表单的运行速度都将明显快于通过聚合框架或任何计算运算符进行的任何操作。

    【讨论】:

    • 尼尔,这是一个了不起的回应。感谢您展示了实现同一目标的多种方法,真的让我对解决这个问题的方法有不同的看法。我不知道$arrayElemAt$indexOfArray 运算符,这对您回复的第一部分有很大帮助。我之前也看过$slice,但对如何使用排序数组感到困惑。您的回答为我解决了很多问题,我非常感谢您抽出宝贵的时间!
    猜你喜欢
    • 1970-01-01
    • 2013-05-06
    • 1970-01-01
    • 1970-01-01
    • 2015-01-14
    • 1970-01-01
    • 1970-01-01
    • 2018-07-03
    • 2016-09-27
    相关资源
    最近更新 更多