是否有一种方法可以在mongo中使用project
或group
将文档或子文档的数组转换为以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上运行查询会更高效。
答案 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代码中进行“联接”,即使它对您隐藏了正在做的事情。