提问者:小点点

使用命名键查询和转换文档


我是一个老SQL,现在正在使用现有的Mongo数据库。

与其使用嵌入文档的数组,关键信息是数组元素中的字段之一,每个嵌入文档都是它自己的字段,键作为字段名。

假设一个名为的集合bank_branches。而不是这样:

{
  branch : "St. Louis",
  branch_employees : [
    { 
       name : "Mary",
       id : "M12345"
       hire_date : Date("2010-05-20")
    },
    {
       name :  "John",
       id : "J29876",
       hire_date : Date("2015-03-23")
    }
  ]
},
{
  branch : "Jefferson City",
  branch_employees : [
    { 
       name : "Lisa",
       id : "L87653"
       hire_date : Date("2016-01-07")
    }
  ]
}

…我们有这样的文件:

{
  branch : "St. Louis",
  branch_employees : {
     M12345 : {
        name : "Mary",
        hire_date : Date("2010-05-20")
     },
     J29876 :  {
       name :  "John",
       hire_date : Date("2015-03-23")
     }
 }
 {
   branch : "Jefferson City",
   branch_employees : {
      L87653 : {
        name : "Lisa",
        hire_date : Date("2016-01-07")
      }
   }
}

(这是一个发明的结构来显示问题。)

在MongoDB聚合管道或其他方式中,是否有任何方法可以执行以下任一操作?

>

  • 查询员工嵌入文档的组件字段,这样我就可以,例如,获取2016年入职员工的所有分支机构,而无需提前知道所有嵌入文档的所有字段名?

    松开这些对象,这样我就可以拥有一个分支机构员工文档的数组,同样不需要提前知道所有嵌入文档的所有字段名?(MongoDB$unstyle管线作业只适用于数组。)

    我怀疑第一个可能可以用$wherejavascript和/或自定义javascript存储函数来解决。(我以前从未使用过存储函数。)但我怀疑第二个只能通过编程来解决。

    我可以通过编写Python和迭代来满足我的用例。但是我宁愿编写查询来查找记录,也不愿以编程方式过滤它们。(唯一保证bug的代码是你不必编写的代码。)

    有人有建议吗?非常感谢,事先。


  • 共1个答案

    匿名用户

    我可以对此喋喋不休,但你似乎至少已经知道最好的答案。将数据转换为数组。“查询”文档的唯一方法基本上包括在服务器上操作文档(每次文档处理),以强制将这些“命名键”转换为数组。

    在最近的MongoDB版本中有更多的现代方法,这意味着你不必使用$where,但是仍然有一个主要的警告,这仍然是错误的形式,你只是“不能使用索引”来加速查询结果。

    在你的问题的基本解决:

    如果您想根据分支机构员工雇佣日期的条件“查找文件”,那么您可以使用带有$where的表达式,如下所示:

    db.bank_branches.find(
      function() {
        return Object.keys(this.branch_employees).some(e =>
          e.hire_date => new Date("2016-01-01") && e.hire_date < new Date("2017-01-01")
        )
      }
    )
    

    我们使用JavaScript评估的主要原因是,您需要以查询DSL无法表达的方式“遍历文档的键”,因此需要针对每个文档评估一个条件,而不是使用索引。

    对于自MongoDB 3.4.4以来的最新版本,您可以使用$object tToArray来实现与Object. key()几乎相同的目的,并将其用作$redact聚合管道阶段中表达式的一部分:

    db.bank_branches.aggregate([
      { "$redact": {
        "$cond": {
          "if": {
            "$anyElementTrue": {
              "$map": {
                "input": { "$objectToArray": "$branch_employees" },
                "in": {
                  "$and": [
                    { "$gte": [ "$$this.hire_date", new Date("2016-01-01") ] },
                    { "$lt": [ "$$this.hire_date", new Date("2017-01-01") ] }
                  ]
                }
              }
            }
          },
          "then": "$$KEEP",
          "else": "$$PRUNE"
        }
      }}
    ])
    

    $redact本质上评估一个条件,并通过$$KEEP返回或通过$$PRUNE根据条件的布尔结果“删除”文档。这类似于$where,只是它是一个本机运算符,而不是使用解释的JavaScript,本质上是“整体”查询条件,而不是$where,后者在技术上只是另一个查询参数,可以与其他条件一起使用。

    从MongoDB 3.6中,我们得到$expr,它允许它缩短,甚至用作与$where相同的“附加参数”语法,除非使用本机编码运算符:

    db.bank_branches.find({
      "$expr": {
        "$anyElementTrue": {
          "$map": {
            "input": { "$objectToArray": "$branch_employees" },
            "in": {
              "$and": [
                { "$gte": [ "$$this.hire_date", new Date("2016-01-01") ] },
                { "$lt": [ "$$this.hire_date", new Date("2017-01-01") ] }
              ]
            }
          }
        }
      }
    })
    

    但它们基本上仍然很可怕,因为您所能做的就是“扫描整个集合”以获得结果。所以更好的“数组”语法是:

    db.bank_branches.find({
      "branch_employees": {
        "$elemMatch": {
          "hire_date": { "$gte": new Date("2016-01-01"), "$lte": new Date("2017-01-01") }
        }
      }
    })
    

    它当然可以使用索引,因为"branch_employees.hire_date"的路径是一致的,并且不使用“命名键”作为所需属性的中间路径的一部分。这是您想要这个结构的主要原因

    为了实际获得该“数组形状”的文档,那么我们应该通过查询的构造方式得到一些指示。

    因此,在第一个选项中,如果写入“新集合”是您的一个选项,那么在现代版本中,您应该能够简单地在服务器本身上完成整个转换:

    db.bank_branches.aggregate([
      { "$project": {
        "branch": 1,
        "branch_employees": {
          "$map": {
            "input": { "$objectToArray": "$branch_employees" },
            "in": {
              "$arrayToObject": {
                "$concatArrays": [
                  [{ "k": "id", "v": "$$this.k" }],
                  { "$objectToArray": "$$this.v" }
                ] 
              }
            }
          }
        }
      }},
      { "$out": "new_branches" }
    ])
    

    或者,如果您没有可用的现代运算符或根本无法写入新集合,则基本上循环集合并写回属性的新数据:

    var ops = [];
    
    db.bank_branches.find({ "branch_employees.0": { "$exists": false } }).forEach( doc => {
    
      ops.push({
        "updateOne": {
          "filter": { "_id": doc._id },
          "update": {
            "$set": {
              "branch_employees": Object.keys(doc.branch_employees).map(id =>
                Object.assign({ id },doc.branch_employees[id])
              )
            }
          }
        }
      });
    
      if ( ops.length > 1000 ) {
        db.bank_branches.bulkWrite(ops);
        ops = [];
      }
    
    })
    
    
    if ( ops.length > 0 ) {
      db.bank_branches.bulkWrite(ops);
      ops = [];
    }
    

    两者本质上都使用相同的技术,即获取对象子键的值并将其与底层对象合并,作为作为“数组”返回的元素。

    N.在$where表达式中运行的代码在JavaScript中并在服务器上运行,因此它仍然是一个“语言无关的解决方案”,其中要评估的“JavaScript”实际上是以其他语言的“字符串”提交的。

    其他表达式基本上分解为所选语言中的BSON表示。Python和Ruby在语法方面与JavaScript几乎相同,并且相同的BSON约定适用于任何地方。

    这里给出的其他例程用于数据的“转换”,作为“一次性”操作,它应该始终足以在shell的JavaScript执行环境中运行。

    因此,没有“需要”使用JavaScript($where表达式除外),但此处以通用格式给出了示例,每个人都可以在MongoDB安装提供的shell中运行。