Så du mangler faktisk nogle begreber her, når du beder om at "befolke" på et aggregeringsresultat. Det er typisk ikke, hvad du rent faktisk gør, men for at forklare pointerne:
-
Outputtet af
aggregate()
er i modsætning til enModel.find()
eller lignende handling, da formålet her er at "omforme resultaterne". Dette betyder grundlæggende, at den model, du bruger som kilde til aggregeringen, ikke længere betragtes som den model ved output. Dette er endda sandt, hvis du stadig beholdt den nøjagtige samme dokumentstruktur på output, men i dit tilfælde er output klart anderledes end kildedokumentet alligevel.Det er i hvert fald ikke længere en forekomst af
Garanti
model du henter fra, men bare en almindelig genstand. Vi kan omgå det, efterhånden som vi berører det senere. -
Det vigtigste her er nok, at
populate()
er noget "gammel hat" alligevel. Dette er egentlig bare en bekvemmelighedsfunktion, der blev tilføjet til Mongoose tilbage i de meget tidlige dage af implementering. Det eneste, det virkelig gør, er at udføre "en anden forespørgsel" på den relaterede data i en separat samling, og fletter derefter resultaterne i hukommelsen til det originale samlingsoutput.Af mange grunde er det i de fleste tilfælde ikke rigtig effektivt eller endda ønskeligt. Og i modsætning til den populære misforståelse er dette IKKE faktisk et "join".
For et rigtigt "join" bruger du faktisk
$lookup
aggregeringspipeline-stadiet, som MongoDB bruger til at returnere de matchende varer fra en anden samling. I modsætning tilpopulate()
dette gøres faktisk i en enkelt anmodning til serveren med et enkelt svar. Dette undgår netværksomkostninger, er generelt hurtigere og giver dig som en "rigtig joinforbindelse" mulighed for at gøre ting, derbefolker()
ikke kan gøre.
Brug $lookup i stedet
Den meget hurtige version af det, der mangler her, er, at i stedet for at forsøge at populate()
i .then()
efter resultatet er returneret, hvad du i stedet gør, er at tilføje $lookup
til rørledningen:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Bemærk, at der er en begrænsning her i, at output fra $ opslag
er altid et array. Det er ligegyldigt, om der kun er én relateret vare eller mange, der skal hentes som output. Pipelinestadiet vil lede efter værdien af "localField"
fra det aktuelle dokument, der præsenteres, og brug det til at matche værdier i "foreignField"
specificeret. I dette tilfælde er det _id
fra sammenlægningen $group
mål til _id
af den udenlandske samling.
Da outputtet altid er et array som nævnt vil den mest effektive måde at arbejde med dette på i dette tilfælde være blot at tilføje en $unwind
trin direkte efter $lookup
. Alt dette vil gøre det til at returnere et nyt dokument for hver genstand, der returneres i målarrayet, og i dette tilfælde forventer du, at det er et. I det tilfælde, hvor _id
ikke matches i den udenlandske samling, ville resultaterne uden match blive fjernet.
Som en lille bemærkning er dette faktisk et optimeret mønster som beskrevet i $lookup + $unwind Coalescence
inden for kernedokumentationen. En særlig ting sker her, hvor $unwind
instruktion er faktisk flettet ind i $lookup
drift på en effektiv måde. Det kan du læse mere om der.
Bruger udfylde
Ud fra ovenstående indhold burde du grundlæggende kunne forstå hvorfor populate()
her er den forkerte ting at gøre. Bortset fra det grundlæggende faktum, at output ikke længere består af Garanti
modelobjekter, kender denne model egentlig kun til fremmede elementer beskrevet på _accountId
egenskab, som alligevel ikke findes i outputtet.
Nu kan faktisk definere en model, som kan bruges til eksplicit at caste output-objekterne til en defineret outputtype. En kort demonstration af en ville indebære at tilføje kode til din ansøgning til dette som:
// 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');
Denne nye Output
model kan derefter bruges til at "caste" de resulterende almindelige JavaScript-objekter ind i Mongoose Documents, så metoder som Model.populate()
kan faktisk kaldes:
// 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);
Siden Output
har et skema defineret, der er opmærksom på "referencen" på _id
feltet i dets dokumenter Model.populate()
er klar over, hvad den skal gøre og returnerer varerne.
Pas dog på, da dette faktisk genererer en anden forespørgsel. dvs.:
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: {} })
Hvor den første linje er det samlede output, og derefter kontakter du serveren igen for at returnere den relaterede Konto
modelindtastninger.
Oversigt
Så det er dine muligheder, men det burde være ret klart, at den moderne tilgang til dette i stedet er at bruge $lookup
og få et rigtigt "join" hvilket ikke er det populate()
faktisk gør.
Inkluderet er en liste som en fuldstændig demonstration af, hvordan hver af disse tilgange faktisk fungerer i praksis. Nogle kunstneriske licenser er taget her, så de repræsenterede modeller er muligvis ikke præcis det samme som du har, men der er nok til at demonstrere de grundlæggende begreber på en reproducerbar måde:
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()
}
})()
Og det fulde output:
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"
}
]
}
]