Така че всъщност пропускате някои понятия тук, когато поискате „попълване“ на резултат от агрегиране. Обикновено това не е това, което всъщност правите, а за да обясните точките:
-
Резултатът от
aggregate()
е различно отModel.find()
или подобно действие, тъй като целта тук е да се „преоформят резултатите“. Това основно означава, че моделът, който използвате като източник на агрегацията, вече не се счита за този модел на изхода. Това е вярно дори ако все още сте поддържали точно същата структура на документа при извеждане, но във вашия случай изходът така или иначе е ясно различен от изходния документ.Във всеки случай това вече не е екземпляр от
Гаранцията
модел, от който се снабдявате, но просто обикновен обект. Можем да заобиколим това, като се спрем на по-късно. -
Вероятно основният момент тук е, че
populate()
е донякъде "стара шапка" така или иначе. Това наистина е просто удобна функция, добавена към Mongoose още в първите дни на внедряването. Всичко, което наистина прави, е да изпълни „друга заявка“ за свързаните данни в отделна колекция и след това обединява резултатите в паметта към оригиналния изход на колекцията.По много причини това не е наистина ефективно или дори желателно в повечето случаи. И противно на популярното погрешно схващане, това еНЕ всъщност „присъединяване“.
За истинско „присъединяване“ всъщност използвате
$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
}}
Имайте предвид, че тук има ограничение в това, че изходът на $ търсене
е винаги масив. Няма значение дали има само един свързан елемент или много, които да бъдат извлечени като изход. Етапът на тръбопровода ще търси стойност на "localField"
от текущия представен документ и го използвайте за съпоставяне на стойности в "foreignField"
посочени. В този случай това е _id
от агрегата $group
насочете към _id
на чуждестранната колекция.
Тъй като изходът е винаги масив както споменахме, най-ефективният начин за работа с това за този случай би бил просто да добавите $unwind
етап непосредствено след $lookup
. Всичко това ще направи така, че да върне нов документ за всеки елемент, върнат в целевия масив, и в този случай вие очаквате да бъде един. В случай, че _id
не съответства в чуждестранната колекция, резултатите без съвпадения ще бъдат премахнати.
Като малка бележка, това всъщност е оптимизиран модел, както е описано в $lookup + $unwind Coalescence
в рамките на основната документация. Специално нещо се случва тук, когато $unwind
инструкцията всъщност се обединява в $lookup
работа по ефективен начин. Можете да прочетете повече за това там.
Използване на попълване
От горното съдържание трябва да можете да разберете основно защо populate()
тук е грешното нещо, което трябва да направите. Освен основния факт, че резултатът вече не се състои от Гаранция
моделни обекти, този модел наистина знае само за чужди елементи, описани в _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');
Този нов Изход
след това моделът може да се използва, за да се „прехвърлят“ получените обикновени JavaScript обекти в Mongoose Documents, така че методи като 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);
Тъй като Изход
има дефинирана схема, която е наясно с "референцията" на _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: {} })
Където първият ред е обобщеният изход и след това се свързвате отново със сървъра, за да върнете свързания Акаунт
записи на модела.
Резюме
Така че това са вашите възможности, но трябва да е доста ясно, че съвременният подход към това е вместо това да използвате $lookup
и получете истинско „присъединяване“ което не е това, което populate()
всъщност прави.
Включен е списък като пълна демонстрация на това как всеки от тези подходи действително работи на практика. Някакъв художествен лиценз е взето тук, така че представените модели може да не са точно същото като това, което имате, но има достатъчно, за да демонстрирате основните концепции по възпроизводим начин:
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', '[email protected]'],
['Second Account', 'Second Person', '[email protected]'],
['Third Account', 'Third Person', '[email protected]']
].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: '[email protected]', __v: 0 }, { _id: 5bf4b591a06509544b8cf75c, name: 'Second Account', contactName: 'Second Person', contactEmail: '[email protected]', __v: 0 }, { _id: 5bf4b591a06509544b8cf75d, name: 'Third Account', contactName: 'Third Person', contactEmail: '[email protected]', __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": "[email protected]",
"__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": "[email protected]",
"__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": "[email protected]",
"__v": 0
},
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
]
},
{
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "[email protected]",
"__v": 0
},
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
]
}
]