如何在MongoDB中将子文档展平为根级别?

54
例如,如果我有这样的一个文件。
{
  a: 1,
  subdoc: {
    b: 2,
    c: 3
  }
}

我该如何将其转换为这样的格式?(不使用project

{
  a: 1,
  b: 2,
  c: 3
}

1
你为什么坚持不使用“项目”来完成它?请回答这个问题,因为你的理由可能也会排除其他可能的答案。 - Philipp
1
@Philipp 在子文档中有30多个元素,所以project意味着要输入很多内容。 - zachguo
1
一个很好的理由是,子文档可能会随着时间的推移而发生变化,因此,如果后来出现了“d”,并且您希望所有子文档都进入根,则指定“b”和“c”不是一种好的方式。 - Rafael Antonio Pólit
2
从MongoDB 3.4开始,有一个聚合管道运算符叫做$replaceRoot,它允许您将subdoc作为新的$$ROOT。但是,它会完全替换根文档。我不知道如何将subdoc与原始根文档中已存在的字段合并。也许这可以给某些人提供提示... - maganap
在MongoDB 3.2版本中,是否有$replaceRoot的任何替代方法? - Prabhat Mishra
仍在等待一种优雅的方法来实现这一点,即使我讨厌投射所有字段。 - virtuvious
8个回答

46
你可以使用MongoDB投影,即$project聚合框架管道运算符。这是推荐的方式。如果你不想使用project,请查看链接

db.collection.aggregation([{$project{ . . }}]);

以下是你的案例示例:
db.collectionName.aggregate
([
    { 
      $project: { 
        a: 1, 
        b: '$subdoc.b', 
        c: '$subdoc.c'
      } 
    }
]);

给你预期的输出,即。
    {
        "a" : 1,
        "b" : 2,
        "c" : 3
    }

38

您可以使用$replaceRoot,并将其与$addFields阶段一起使用,如下所示:

db.collection.aggregate([
    { "$addFields": { "subdoc.a": "$a" } },
    { "$replaceRoot": { "newRoot": "$subdoc" }  }
])

15
如果你在根文档有很多字段,你可以使用特殊变量$$ROOTmergeObjects函数进行合并,示例代码如下:db.collection.aggregate([ {$replaceRoot: { newRoot: { $mergeObjects: ["$$ROOT", "$subdoc"] } } }, {$project: { subdoc: 0 } }, ])注意不要改变原本的意思,并且使内容更加通俗易懂。 - 0zkr PM

25
我们可以使用$replaceWith$replaceRoot的别名)以及$mergeObjects运算符来实现这一点。
let pipeline = [
   {
      "$replaceWith": {
         "$mergeObjects": [ { "a": "$a" }, "$subdoc" ]
      }
   }
];
或者
let pipeline = [
    {
        "$replaceRoot": {
            "newRoot": {
                "$mergeObjects": [{ "a": "$a" }, "$subdoc" ]
            }
        }
    }
];

db.collection.aggregate(pipeline)

9

您还可以在$mergeObjects中使用$$ROOT

{ $replaceWith: {
        $mergeObjects: [ "$$ROOT", "$subdoc" ]
} }

4

Mongo 4.2 开始,聚合操作符 $replaceWith 可以用来用另一个文档(在我们的情况下是子文档)替换文档,这相当于 @chridam 提到的 $replaceRoot 的简化语法。

因此,我们可以首先在子文档中包含根字段,以继续使用 $set 操作符(也是 Mongo 4.2$addFields 的别名),然后使用 $replaceWith 将整个文档替换为子文档:

// { a: 1, subdoc: { b: 2, c: 3 } }
db.collection.aggregate([
  { $set: { "subdoc.a": "$a" } },
  { $replaceWith: "$subdoc" }
])
// { b: 2, c: 3, a: 1 }

1
如果您想查询嵌套数组元素并在根级别显示它,则应该使用 $unwind 两次,如果有两个嵌套数组。
db.mortgageRequests.aggregate( [
       { $unwind: "$arrayLevel1s" },
       { $unwind: "$arrayLevel1s.arrayLevel2s" },
       { $match: { "arrayLevel1s.arrayLevel2s.documentid" : { $eq: "6034bceead4ed2ce3951f38e" } } },
       { $replaceRoot: { newRoot: "$arrayLevel1s.arrayLevel2s" } }
    ] )

0

我想最简单的方法是在返回结果后使用map进行修改。

collection.mapFunction = function(el) {
  el.b = el.subdoc.b;
  el.c = el.subdoc.c
  delete(el.subdoc);
  return el;
}

...

var result = await collection.aggregate(...).toArray();
result = result.map(collection.mapFunction);

我认为你不应该这样做。 - styvane
嗯,这很糟糕,因为它在应用程序内存上运行而不是在数据库级别上运行,但我不必在$project子句中重写数据库的每个字段。 - ariel

-1

你可以使用$set$unset来实现这个功能。

db.collection.update({},{ $set: { "b": 2, "c": 3 }, $unset: { "subdoc": 1 } })

当然,如果集合中有大量文档,那么就像之前一样,您需要循环集合文档以获取值。在更新操作中,MongoDB没有单独引用字段值的方法:
db.collection.find({ "subdoc": {"$exists": 1 } }).forEach(function(doc) {
    var setDoc = {};
    for ( var k in doc.subdoc ) {
        setDoc[k] = doc.subdoc[k];
    }
    db.collection.update(
        { "_id": doc._id },
        { "$set": setDoc, "$unset": "subdoc" }
    );
})

这也使用了一些安全的 $exists 用法,以确保您选择的文档中实际存在“subdoc”键。


1
那么在mongoDB中没有内置的运算符/函数来完成这个操作,对吗? - zachguo
@svg 运算符就是我所提到的。你不能仅通过更新操作获取字段的值,这也是我所说的。 - Neil Lunn
1
好的,明白了,谢谢。我实际上是指一个单一的运算符,在一次操作中完成。抱歉我没有表达清楚。 - zachguo
顺便提一下,巧妙地使用 $set$unset。我担心循环比 $project 的性能要差,对吗?如果是这样,我就坚持使用 $project,但需要打更多的字(子文档中有30个以上的元素)。 - zachguo
1
@svg 这两件事情完全不同。你要么是在操作实际文档,要么是进行投影以重新塑造特定的结果,而这只能通过聚合来完成。决定你想做哪一个。修改你的文档或以不同的方式呈现结果。 - Neil Lunn
是的,我知道。我只是在为分析准备数据,是否要就地修改对我来说并不重要。再次感谢。 - zachguo

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