MongoDB 聚合框架:按嵌套文档进行匹配

12

我有以下文件清单:

{
    "_id" : "Tvq579754r",
    "name": "Tom",
    "forms": {
           "PreOp":{
             "status":"closed"          
           },

           "Alert":{
             "status":"closed"          
           },

           "City":{
              "status":"closed"         
           },

          "Country":{
             "status":"closed"          
          } 
    }
},
....
{
    "_id" : "Tvq444454j",
    "name": "Jim",
    "forms": {
          "Jorney":{
             "status":"closed"          
           },

          "Women":{
             "status":"void"            
          },

         "Child":{
            "status":"closed"           
         },

         "Farm":{
           "status":"closed"            
         }  
     }
}

我希望通过它们的“状态”字段(“forms.name_of_form.status”)对它们进行过滤。我需要获取所有没有“forms.name_of_form.status”等于“void”的文档。

期望的结果是(没有作废表单状态的文档):

{
    "_id" : "Tvq579754r",
    "name": "Tom",
    "forms": {
           "PreOp":{
             "status":"closed"          
           },

           "Alert":{
             "status":"closed"          
           },

           "City":{
              "status":"closed"         
           },

          "Country":{
             "status":"closed"          
          } 
    }
}

你需要获取所有文档或所有 _ids 吗?此外,您需要过滤掉具有特定表单且状态为“无效”或任何状态“无效”的文档吗? - Jinxcat
表单名称列表是否是有限的(已指定)? - Jinxcat
不,表单名称可以在将来更改。我不必依赖于表单名称。 - Andrii Furmanets
作为结构和数据使用模式的良好示例。谢谢询问。 - Neil Lunn
@Jinxcat,你看到最终答案了吗?我知道你喜欢这种东西 :) - Neil Lunn
显示剩余4条评论
2个回答

25

如果不提前知道所有可能的表单名称并在查询中使用它们,就无法查询这个结构中想要的结果。无论如何都会非常混乱。尽管如此,请继续阅读,因为我将解释如何做到这一点。

这些文档的结构存在问题,将阻止您进行任何合理的查询分析。按照目前的方式,您必须知道所有可能的表单名称字段才能筛选出任何内容。

您当前的结构包含一个子文档的表单,其中每个键都包含另一个子文档,其中只有一个属性状态。这很难遍历,因为您的表单元素对于创建的每个文档都具有任意结构。这意味着降低到您要比较的状态信息的模式对于集合中的每个文档都会发生变化。

这是路径的意思。要获取任何元素中的状态,您必须执行以下操作

表单 -> PreOp -> 状态

表单 -> Alert -> 状态

第二个元素始终在更改。没有方法可以像通配符一样使用这样的东西,因为命名被认为是明确的。

这可能被认为是实现将数据从您的表单序列化的简单方法,但我看到了一个更灵活的选择。您需要的是可以按照标准模式遍历的文档结构。在设计中始终值得考虑这一点。例如:

{
    "_id" : "Tvq444454j",
    "name": "Jim",
    "forms": [
        {
             "name": "Jorney",
             "status":"closed"          
        },
        {
            "name": "Women",
            "status":"void"            
        },
        {
            "name": "Child",
            "status":"closed"           
        },
        {
            "name": "Farm",
            "status":"closed"            
        }  
    ]
}
因此,文档的结构已更改,使forms元素成为一个数组,并且不再将状态字段放置在命名为“表单字段”的键下,而是将数组的每个成员作为包含“表单字段”名称状态的子文档。因此,标识符和状态仍然配对,但现在表示为子文档。最重要的是,这会更改访问这些密钥的路径,因为现在对于两个字段名称及其状态,我们可以执行以下操作:

Forms-> 状态

Forms-> 名称

这意味着您可以查询以查找form中所有字段的名称或所有form中的status字段,甚至是具有特定name字段和特定status的所有文档。这比原始结构能做的要好得多。
现在在您的特定情况下,您想要获取所有字段都不为void的文档。现在,无法在单个查询中执行此操作,因为没有运算符可以以这种方式比较数组中的所有元素并查看它们是否相同。但是,有两种方法可以采取:
第一种方法可能不太高效,即查询包含forms中具有状态“void”的元素的所有文档。使用所得到的文档 ID,您可以发出另一个查询,返回没有指定的ID的文档。
db.forms.find({ "forms.status": "void" },{ _id: 1})

db.forms.find({ _id: $not: { $in: [<Object1>,<Object2>,<Object3>,... ] } })

考虑到结果的规模,这可能是不可能的,而且通常不是一个好主意,因为排除运算符$not基本上强制对集合进行全扫描,所以您无法使用索引。

另一种方法是使用聚合管道,如下所示:

db.forms.aggregate([
    { "$unwind": "$forms" },
    { "$group": { "_id": "$_id", "status": { "$addToSet": "$forms.status" }}},
    { "$unwind": "$status" },
    { "$sort": { "_id": 1, "status": -1 }},
    { "$group": { "_id": "$_id", "status": { "$first": "$status"}}},
    { "$match":{ "status": "closed" }}
])
当然这只会返回匹配的文档的_id,但您可以使用$in进行查询并返回所有匹配的文档。这比以前使用的排除运算符更好,现在我们可以使用索引来避免完整的集合扫描。
作为最终方法和考虑到最佳性能,您可以再次更改文档,以便在顶层保留表单中任何字段的“状态”是“void”或“closed”的信息。因此,在顶层,仅当所有项目都为“closed”时,该值才为closed,“void”表示某些内容为空等等。
最后一种方法意味着进一步的编程更改,并且所有对forms项目的更改都需要更新此字段,以维护“状态”。但这是找到所需文档的最有效方法,可能值得考虑。
编辑: 除了更改文档以具有主状态外,修订结构上最快的查询表单实际上是:
db.forms.find({ "forms": { "$not": { "$elemMatch": { "status": "void" } } } })

2
在使用聚合方法时,我忘记了一件事(扇自己一个耳光),你可以将整个文档作为 _id 进行 $project,因为它已经是唯一的,并且这是分组的关键。这样,通过添加最终的 $project 并在原始形式中重新建立 _id 的键,您可以将文档恢复到原始状态并消除使用 _id 键发出另一个查询的需要。因为我太习惯于在聚合中进行转换而忘记了这一点。 - Neil Lunn
3
非常感谢。我花了很多时间搜索这种方法。 - Andrii Furmanets

0

如果您有原始结构,并且需要在聚合管道中使用它,可以使用$objectToArray,以便像这样用已知的键替换未知的键:

  1. 使用$objectToArray创建已知键。
  2. 创建所有状态formsData的数组
  3. $filter formsData并计算数组中void的数量作为voidCount
  4. $match仅匹配具有voidCount: 0的文档
  5. 格式化
db.collection.aggregate([
  {$addFields: {formsData: {$objectToArray: "$forms"}}},
  {$set: {formsData: "$formsData.v.status"}},
  {
    $set: {
      voidCount: {
        $size: {
          $filter: {
            input: "$formsData",
            as: "item",
            cond: {$eq: ["$$item", "void"]}
          }
        }
      }
    }
  },
  {$match: {voidCount: 0}},
  {$unset: ["voidCount", "formsData"]}
])

示例游乐场


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接