如何使用新 ID mongoexport 和 mongoimport 集合但保持关系

How to mongoexport and mongoimport collections with new ids but keeping the relation

我使用 mongoexport 导出 2 个集合:

mongoexport -h -u -p --db dev -c parent -q '{_id: ObjectId("5ae1b4b93b131f57b33b8d11")}' -o parent.json

mongoexport -h -u -p --db dev -c children -q '{parentId: ObjectId("5ae1b4b93b131f57b33b8d11")}' -o children.json

从第一个我得到一条记录(通过 ID 选择),从第二个我得到很多记录(通过 parentId 选择,它是第一个的 ID。

我如何使用 mongoimport 导入具有新 ID 但保持关系的那些 -> parent.id = child.parentId

?

TLDR;跳到解决这个问题的部分,或者如果你想消除误解,请实际阅读。

背景

让我们澄清这里的基本误解,因为 MongoDB "itself" 没有 关系的概念,不管怎样。您所指的 "relation" 只是一个集合中 属性 中记录的值,而 "refers" 是另一个集合中存在的另一个值.这些 "foreign keys" 没有对 MongoDB 中的任何类型的约束强制执行,它定义数据是 "related" , 它们只是 "values".

一旦你接受了这个事实,那么它应该是 self apparent 为什么像 mongoexport or even basic query operations like find() 这样的工具真的不知道仅仅因为你把一个值放在某个地方它是 "meant to" 去从其他地方获取数据。

曾几何时(并且在较小程度上仍然存在)一个名为 DBRef, which stored not just a "value" but also some detail as to where that value resided in a "referential" term. This could be collection or database and collection. The concept however remains relatively short lived and not useful in modern context. Even with this "metadata" stored, the database itself covered no concept of "relation" to the data. Retrieval still remained a "client concept" where some drivers had the ability to see a DBRef 的概念然后 "resolve" 它通过向服务器发出另一个查询来检索 "related" 数据.

当然这是 "full of holes",这个概念被放弃了,取而代之的是更现代的概念,特别是 $lookup

现代建筑

这一切真正归结为,就 MongoDB 本身而言,"relation" 实际上是 "client concept",在某种意义上,"external entity" 实际上通过您定义为 "equal references" 的数据做出 "this" 与 "that" 相关的决定。

没有 "database rules" 强制执行此操作,因此与各种传统 RDBMS 解决方案相比,MongoDB 的 "object store" 本质本质上说 "...那不是我的工作,委托给其他人,这通常意味着您在用于访问数据库的"client"逻辑中定义它。

但是,"some tools" 允许 "server" 实际按照该逻辑行事。这些基本上围绕 $lookup 展开,这基本上是他 "modern method" 在使用 MongoDB.

时在服务器上执行 "joins"

所以如果你有"related data"你想"export",那么你基本上有几个选择:

  • 使用 $lookup

    创建 "view"

    MongoDB 在 3.2 版本中引入了 "views"。这些本质上是 "aggregation pipelines" 其中 "masquerade" 作为一个集合。对于像 .find() 甚至 mongoexport 这样的正常操作的所有目的,这个管道看起来就像一个集合,可以这样访问。

    db.createView("related_view", "parent", [
      { "$lookup": {
        "from": "children",
        "localField": "_id",
        "foreignField": "parentId",
        "as": "children"
      }}
    ])
    

    有了 "view",您可以使用为 "view":

    定义的名称简单地调用 mongoexport
    mongoexport -c related_view -o output.json
    

    正如 $lookup 所做的那样,每个 "parent" 项现在将包含一个数组,其中 "related" 内容来自 "children" 的外键。

    由于 $lookup 将输出生成为 BSON 文档,因此与所有 MongoDB 一样适用相同的限制,因为生成的 "join" 在任何文档中都不能超过 16MB。因此,如果数组导致 parent 文档增长超过此限制,那么使用输出作为嵌入在文档中的 "array" 不是一个选项。

    对于这种情况,您通常会使用 $unwind 以便 "de-normalize" 输出内容就像典型的 SQL 连接一样。在这种情况下,将为每个 "related" 成员复制 "parent" 文档,输出文档与相关 children 的文档匹配,但包含所有 parent 信息和 "children" 作为单数嵌入 属性.

    这只是意味着将$unwind添加到这样的"view":

    db.createView("related_view", "parent", [
      { "$lookup": {
        "from": "children",
        "localField": "_id",
        "foreignField": "parentId",
        "as": "children"
      }},
      { "$unwind": "$children" }
    ])
    

    因为我们基本上只是为每个 "related child" 输出一个文档,所以不太可能违反 BSON 限制。对于 parent 和 child 的非常大的文档,它仍然是一种可能性,尽管很少见。对于那种情况会有不同的处理方式,我们稍后会提到。

  • 直接在脚本中使用$lookup

    如果你没有 MongoDB 支持 "views" 的版本,但你仍然有 $lookup 并且没有 BSON 限制的限制,你基本上仍然可以 "script"使用 mongo shell 调用聚合管道并输出为 JSON.

    过程类似,但我们没有使用 "view" 和 mongoexport,而是手动包装了一些可以从命令行调用到 shell 中的命令:

    mongo --quiet --eval '
        db.parent.aggregate([ 
          { "$lookup": {
            "from": "children",
            "localField": "_id",
            "foreignField": "parentId",
            "as": "children"
          }}
        ]).forEach(p => printjson(p))'
    

    再一次与您可以选择 $unwind 以及管道中的过程大致相同,如果您想要的话

  • 编写"join"

    脚本

    如果你 运行 在没有 $lookup 支持的 MongoDB 实例上(你不应该,因为低于 3.0 没有更多的官方支持)或者你确实有"join" 会为每个超过 BSON 限制的 "parent" 文档创建数据,那么另一种选择是 "script" 通过执行查询获取 "related" 整个连接过程] 数据并输出。

    mongo --quiet --eval '
        db.parent.find().forEach(p =>
          printjson(
            Object.assign(p, {
            children: db.children.find({ parentId: p._id }).toArray()
            })
          )
        )'
    

    甚至 "unwound" 或 "de-normalized" 形式

    mongo --quiet --eval '
        db.parent.find().forEach(p =>
          db.children.find({ parentId: p._id }).forEach(child =>
            printjson(Object.assign(p,{ child }))
          )
        )'
    

总结

底线是 "MongoDB itself" 不了解 "relations",具体细节由您决定。无论是以 "view" 的形式,您都可以访问或通过其他方式定义 "code" 来显式声明此 "relation" 的 "terms",因为就数据库本身担心这根本不以任何其他形式存在。

同样只是为了解决评论中的一个问题,如果您在 "export" 上的意图只是创建一个 "new collection",那么只需创建 "view" 或使用 $out聚合运算符:

db.parent.aggregate([
  { "$lookup": {
    "from": "children",
    "localField": "_id",
    "foreignField": "parentId",
    "as": "children"
  }},
  { "$out": "related_collection" }
])

如果你想 "alter the parent" 使用 "embedded" 数据,然后循环并使用 bulkWrite():

var batch = [];

db.parent.aggregate([
  { "$lookup": {
    "from": "children",
    "localField": "_id",
    "foreignField": "parentId",
    "as": "children"
  }}
]).forEach(p => {
  batch.push({
    "updateOne": {
      "filter": { "_id": p._id },
      "update": {
        "$push": { "children": { "$each": p.children } }
      }
    }
  });

  if (batch.length > 1000) {
    db.parent.bulkWrite(batch);
    batch = [];
  })
});

if (batch.length > 0) {
  db.parent.bulkWrite(batch);
  batch = [];
}

根本没有必要 "export" 仅仅创建一个新的集合或改变一个现有的集合。当您希望将集合实际保留为 "embedded" 数据并且不需要每个请求的 $lookup 开销时,您当然会这样做。但是否 "embed or reference" 的决定完全是另一回事。