将文档数组显示为ID为键的对象

时间:2019-03-05 22:37:19

标签: node.js mongodb mongoose mongodb-query aggregation-framework

是否有一种方法可以在mongo中使用projectgroup将文档或子文档的数组转换为以ObjectId为键,将文档/子文档作为值的对象?

例如:

const book = Schema({
    name: {type: String},
    authors: [{type: mongoose.Schema.Types.ObjectId, ref: 'Author'}]
})

const author = Schema({name: {type: String}})

如果您查询所有图书:

Book.find({}).populate('authors').lean().exec()

然后您得到:

[{
    id: 10,
    name: 'Book 1',
    authors: [{
        id: 1, 
        name: 'Author1'
    }]

},...]

但是我想要这个:

[{
    id: 10,    
    name: 'Book 1',
    authors: {
        1: {id: 1, name: 'Author 1'}
    }
},...]

我知道从mongo查询后遍历对象会做到这一点,但我想在mongo上运行查询会更高效。

1 个答案:

答案 0 :(得分:1)

这里主要考虑的是,您想要的“键”实际上是架构中定义的ObjectId值,而不是真正的“字符串”,这实际上是JavaScript对象的要求,因为所有“键” 必须为“字符串”。对于JavaScript来说,这实际上不是什么大问题,因为JavScript会“字符串化”指定为“键”的任何参数,但这对BSON来说确实很重要,这就是MongoDB实际上“说话”的意思。

因此,您可以使用MongoDB进行此操作,但至少需要MongoDB 4.x才能支持$convert聚合运算符,或者它是 shortcut 方法$toString。这也意味着您实际上不是使用populate(),而是使用MongoDB $lookup

let results = await Books.aggregate([
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "authors",
    "foreignField": "_id",
    "as": "authors"
  }},
  { "$addFields": {
    "authors": {
      "$arrayToObject": {
         "$map": {
           "input": "$authors",
           "in": { "k": { "$toString": "$$this._id" }, "v": "$$this" }
         }
       }
     }
  }}
])

或者,如果您喜欢其他语法:

let results = await Books.aggregate([
  { "$lookup": {
    "from": "authors",
    "let": { "authors": "$authors" },
    "pipeline": [
      { "$match": { "$expr": { "$in": [ "$_id", "$$authors" ] } } },
      { "$project": {
        "_id": 0,
        "k": { "$toString": "$_id" },
        "v": "$$ROOT"
      }}
    ],
    "as": "authors"
  }},
  { "$addFields": {
    "authors": { "$arrayToObject": "$authors" }
  }}
])

哪个会返回类似的内容:

{
        "_id" : ObjectId("5c7f046a7cefb8bff9304af8"),
        "name" : "Book 1",
        "authors" : {
                "5c7f042e7cefb8bff9304af7" : {
                        "_id" : ObjectId("5c7f042e7cefb8bff9304af7"),
                        "name" : "Author 1"
                }
        }
}

因此$arrayToObject会执行实际的“对象”输出,在其中为您提供对象数组,这些对象具有{<1>}和k属性,分别与 key 和< em>值。但是,它必须v中有一个有效的“字符串”,这就是为什么$map对数组内容进行{@ 3}首先格式化的原因。

或者作为替代,您可以在$project"k"自变量内$lookup,而不用稍后再使用$map

使用客户端 JavaScript,翻译过程与此类似:

pipeline

或与let results = await Books.aggregate([ { "$lookup": { "from": Author.collection.name, "localField": "authors", "foreignField": "_id", "as": "authors" }}, /* { "$addFields": { "authors": { "$arrayToObject": { "$map": { "input": "$authors", "in": { "k": { "$toString": "$$this._id" }, "v": "$$this" } } } } }} */ ]) results = results.map(({ authors, ...rest }) => ({ ...rest, "authors": d.authors.reduce((o,e) => ({ ...o, [e._id.valueOf()]: e }),{}) }) )

populate()

注意,但是let results = await Book.find({}).populate("authors"); results = results.map(({ authors, ...rest }) => ({ ...rest, "authors": (!authors) ? {} : authors.reduce((o,e) => ({ ...o, [e._id.toString()]: e }),{}) }) ) populate()确实有很大的不同。 MongoDB $lookup是对服务器的单个请求,该请求返回一个响应。实际上,使用$lookup会调用多个查询,并在客户端JavaScript代码中进行“联接”,即使它对您隐藏了正在做的事情。