Основное соображение здесь заключается в том, что нужные вам «ключи» на самом деле являются ObjectId
значениями, определенными в вашей схеме, а не «строкой», что на самом деле является обязательным требованием для объекта JavaScript, поскольку все «ключи» должен быть "строкой". Для JavaScript это на самом деле не является большой проблемой, так как JavScript будет «структурировать» любой аргумент, заданный как «ключ», но это имеет значение для BSON, что на самом деле MongoDB «говорит»
Таким образом, вы можете сделать это с MongoDB, но вам необходим MongoDB 4.x как минимум для поддержки оператора агрегации $convert
или его ярлык метод $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
выполняет фактический вывод «Объект», где вы предоставляете ему массив объектов со свойствами k
и v
, соответствующими key и значение . Но должен иметь действительную "строку" в "k"
, поэтому вы $map
поверх содержимого массива, чтобы сначала переформатировать его.
В качестве альтернативы вы можете $project
в аргументе pipeline
$lookup
вместо использования $map
позже точно так же.
С на стороне клиента JavaScript, перевод аналогичен процессу:
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()
и $lookup
действительно очень разные. MongoDB $lookup
- это один запрос к серверу, который возвращает один ответ . Использование populate()
фактически вызывает несколько запросов и выполняет "объединение" в клиентском JavaScript-коде , даже если он скрывает то, что он делает от вас.