Таким образом, вы фактически упускаете некоторые понятия здесь, когда просите «заполнить» результат агрегации.Как правило, это не то, что вы на самом деле делаете, а для объяснения пунктов:
Вывод aggregate()
отличается от Model.find()
или аналогичного действия, поскольку цель здесь состоит в том, чтобы "изменить формурезультаты, достижения".В основном это означает, что модель, которую вы используете в качестве источника агрегации, больше не считается этой моделью при выводе.Это даже верно, если вы по-прежнему сохраняете ту же самую структуру документа при выводе, но в вашем случае выход явно отличается от исходного документа в любом случае.
Во всяком случае, он больше не является экземпляром Warranty
модель, из которой вы получаете, но просто обычный объект.Мы можем обойти это, когда коснемся позже.
Вероятно, главное здесь то, что populate()
в некоторой степени "старая шляпа" в любом случае.Это на самом деле просто вспомогательная функция, добавленная в Mongoose еще в первые дни реализации.Все, что он на самом деле делает, - это выполняет «другой запрос» к связанным данным в отдельной коллекции, а затем объединяет результаты в памяти с исходным выводом коллекции.
По многим причинамэто не очень эффективно или даже желательно в большинстве случаев.И вопреки распространенному заблуждению, это NOT на самом деле "соединение".
Для реального "соединения" вы фактически используете стадию конвейера агрегации $lookup
, который MongoDB использует для возврата совпадающих элементов из другой коллекции.В отличие от populate()
это фактически делается в одном запросе к серверу с одним ответом.Это позволяет избежать перегрузок сети, как правило, быстрее и, поскольку «реальное соединение» позволяет вам делать то, что populate()
не может сделать.
Используйте вместо этого $ lookup
.Очень быстрая версия того, что здесь отсутствует, заключается в том, что вместо попытки populate()
в .then()
после возврата результата вы вместо этого добавляете $lookup
к конвейеру:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Обратите внимание, что здесь есть ограничение в том, что вывод $lookup
равен всегда массиву,Неважно, есть ли только один связанный элемент или несколько, которые будут выбраны в качестве выходных данных.Этап конвейера будет искать значение "localField"
из текущего представленного документа и использовать его для сопоставления со значениями в указанном "foreignField"
.В данном случае это _id
от агрегации $group
target до _id
чужой коллекции.
Так как на выходе всегда массив как уже упоминалось, наиболее эффективным способом работы с этим для этого экземпляра было бы просто добавить этап $unwind
непосредственно после $lookup
.Все, что нужно сделать, это вернуть новый документ для каждого элемента, возвращенного в целевой массив, и в этом случае вы ожидаете, что он будет один.В случае, если _id
не соответствует в чужой коллекции, результаты без совпадений будут удалены.
В качестве небольшой заметки, это фактически оптимизированный шаблон, как описано в $ lookup+ $ unwind Coalescence в базовой документации.Здесь происходит нечто особенное, когда инструкция $unwind
фактически эффективно объединяется с операцией $lookup
.Вы можете прочитать больше об этом здесь.
Используя populate
Из приведенного выше контента вы сможете понять, почему populate()
здесь не так.Помимо основного факта, что выходные данные больше не состоят из Warranty
объектов модели, эта модель действительно знает только о посторонних элементах, описанных в свойстве _accountId
, которое в любом случае не существует в выходных данных.
Теперьвы можете на самом деле определить модель, которую можно использовать для явного приведения выходных объектов к определенному типу вывода.Короткая демонстрация одного из них будет включать добавление кода в ваше приложение, например:
// Special models
const outputSchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Account" },
total: Number,
lineItems: [{ address: String }]
});
const Output = mongoose.model('Output', outputSchema, 'dontuseme');
Эту новую модель Output
можно затем использовать для «приведения» результирующих простых объектов JavaScript в документы Mongoose, чтобы можно было вызывать такие методы, как Model.populate()
:
// excerpt
result2 = result2.map(r => new Output(r)); // Cast to Output Mongoose Documents
// Call populate on the list of documents
result2 = await Output.populate(result2, { path: '_id' })
log(result2);
Поскольку Output
имеет определенную схему, которая знает о "ссылке" в поле _id
своих документов, Model.populate()
знает, что ему нужно сделать, и возвращает элементы.
Остерегайтесь, хотя, поскольку это на самом деле генерирует другой запрос.то есть:
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
Где первая строка - совокупный вывод, а затем вы снова связываетесь с сервером для возврата связанных записей модели Account
.
Сводка
Так что это ваши варианты, но должно быть совершенно ясно, что современный подход к этому - вместо этого использовать $lookup
и получить настоящее "соединение" , которое не
включает в себя список того, как на самом деле работает каждый из этих подходов на практике.Здесь взята некоторая художественная лицензия , поэтому представленные модели могут не быть точно такими же, как у вас, но их достаточно для демонстрации основных понятий воспроизводимым способом:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/joindemo';
const opts = { useNewUrlParser: true };
// Sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// Schema defs
const warrantySchema = new Schema({
address: {
street: String,
city: String,
state: String,
zip: Number
},
warrantyFee: Number,
_accountId: { type: Schema.Types.ObjectId, ref: "Account" },
payStatus: String
});
const accountSchema = new Schema({
name: String,
contactName: String,
contactEmail: String
});
// Special models
const outputSchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Account" },
total: Number,
lineItems: [{ address: String }]
});
const Output = mongoose.model('Output', outputSchema, 'dontuseme');
const Warranty = mongoose.model('Warranty', warrantySchema);
const Account = mongoose.model('Account', accountSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
// set up data
let [first, second, third] = await Account.insertMany(
[
['First Account', 'First Person', 'first@example.com'],
['Second Account', 'Second Person', 'second@example.com'],
['Third Account', 'Third Person', 'third@example.com']
].map(([name, contactName, contactEmail]) =>
({ name, contactName, contactEmail })
)
);
await Warranty.insertMany(
[
{
address: {
street: '1 Some street',
city: 'Somewhere',
state: 'TX',
zip: 1234
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Next Billing Cycle'
},
{
address: {
street: '2 Other street',
city: 'Elsewhere',
state: 'CA',
zip: 5678
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Next Billing Cycle'
},
{
address: {
street: '3 Other street',
city: 'Elsewhere',
state: 'NY',
zip: 1928
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Already'
},
{
address: {
street: '21 Jump street',
city: 'Anywhere',
state: 'NY',
zip: 5432
},
warrantyFee: 100,
_accountId: second,
payStatus: 'Invoiced Next Billing Cycle'
}
]
);
// Aggregate $lookup
let result1 = await Warranty.aggregate([
{ "$match": {
"payStatus": "Invoiced Next Billing Cycle"
}},
{ "$group": {
"_id": "$_accountId",
"total": { "$sum": "$warrantyFee" },
"lineItems": {
"$push": {
"_id": "$_id",
"address": {
"$trim": {
"input": {
"$reduce": {
"input": { "$objectToArray": "$address" },
"initialValue": "",
"in": {
"$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
}
},
"chars": " "
}
}
}
}
}},
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
])
log(result1);
// Convert and populate
let result2 = await Warranty.aggregate([
{ "$match": {
"payStatus": "Invoiced Next Billing Cycle"
}},
{ "$group": {
"_id": "$_accountId",
"total": { "$sum": "$warrantyFee" },
"lineItems": {
"$push": {
"_id": "$_id",
"address": {
"$trim": {
"input": {
"$reduce": {
"input": { "$objectToArray": "$address" },
"initialValue": "",
"in": {
"$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
}
},
"chars": " "
}
}
}
}
}}
]);
result2 = result2.map(r => new Output(r));
result2 = await Output.populate(result2, { path: '_id' })
log(result2);
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
И полный вывод:
Mongoose: dontuseme.deleteMany({}, {})
Mongoose: warranties.deleteMany({}, {})
Mongoose: accounts.deleteMany({}, {})
Mongoose: accounts.insertMany([ { _id: 5bf4b591a06509544b8cf75b, name: 'First Account', contactName: 'First Person', contactEmail: 'first@example.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75c, name: 'Second Account', contactName: 'Second Person', contactEmail: 'second@example.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75d, name: 'Third Account', contactName: 'Third Person', contactEmail: 'third@example.com', __v: 0 } ], {})
Mongoose: warranties.insertMany([ { _id: 5bf4b591a06509544b8cf75e, address: { street: '1 Some street', city: 'Somewhere', state: 'TX', zip: 1234 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf75f, address: { street: '2 Other street', city: 'Elsewhere', state: 'CA', zip: 5678 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf760, address: { street: '3 Other street', city: 'Elsewhere', state: 'NY', zip: 1928 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Already', __v: 0 }, { _id: 5bf4b591a06509544b8cf761, address: { street: '21 Jump street', city: 'Anywhere', state: 'NY', zip: 5432 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75c, payStatus: 'Invoiced Next Billing Cycle', __v: 0 } ], {})
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } }, { '$lookup': { from: 'accounts', localField: '_id', foreignField: '_id', as: 'accounts' } }, { '$unwind': '$accounts' }, { '$project': { _id: '$accounts', total: 1, lineItems: 1 } } ], {})
[
{
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
],
"_id": {
"_id": "5bf4b591a06509544b8cf75c",
"name": "Second Account",
"contactName": "Second Person",
"contactEmail": "second@example.com",
"__v": 0
}
},
{
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
],
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "first@example.com",
"__v": 0
}
}
]
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
[
{
"_id": {
"_id": "5bf4b591a06509544b8cf75c",
"name": "Second Account",
"contactName": "Second Person",
"contactEmail": "second@example.com",
"__v": 0
},
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
]
},
{
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "first@example.com",
"__v": 0
},
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
]
}
]