U mist dus eigenlijk enkele concepten wanneer u vraagt om een aggregatieresultaat te "invullen". Meestal is dit niet wat u daadwerkelijk doet, maar om de punten uit te leggen:
-
De uitvoer van
aggregate()
is anders dan eenModel.find()
of soortgelijke actie, aangezien het doel hier is om "de resultaten opnieuw vorm te geven". Dit betekent in feite dat het model dat u gebruikt als de bron van de aggregatie, niet langer als dat model wordt beschouwd bij de uitvoer. Dit geldt zelfs als u nog steeds exact dezelfde documentstructuur bij de uitvoer handhaaft, maar in uw geval is de uitvoer toch duidelijk anders dan het brondocument.Het is in ieder geval niet langer een instantie van de
Warranty
model waar u vandaan haalt, maar slechts een eenvoudig object. We kunnen daar omheen werken als we later terugkomen. -
Waarschijnlijk is het belangrijkste punt hier dat
populate()
is een beetje "oude hoed" hoe dan ook. Dit is eigenlijk gewoon een gemaksfunctie die in de allereerste dagen van implementatie aan Mongoose is toegevoegd. Het enige dat het echt doet, is "een andere zoekopdracht" uitvoeren op de gerelateerde gegevens in een aparte verzameling en voegt vervolgens de resultaten in het geheugen samen met de oorspronkelijke verzamelingsuitvoer.Om tal van redenen is dat in de meeste gevallen niet echt efficiënt of zelfs wenselijk. En in tegenstelling tot de populaire misvatting, is dit NIET eigenlijk een "join".
Voor een echte "join" gebruik je eigenlijk de
$lookup
aggregatiepijplijnfase, die MongoDB gebruikt om de overeenkomende items uit een andere verzameling te retourneren. In tegenstelling totpopulate()
dit gebeurt eigenlijk in een enkel verzoek aan de server met een enkel antwoord. Dit vermijdt netwerkoverhead, is over het algemeen sneller en als een "echte join" kun je dingen doen diepopulate()
kan niet.
Gebruik in plaats daarvan $lookup
De zeer snelle versie van wat hier ontbreekt, is dat in plaats van te proberen om populate()
in de .then()
nadat het resultaat is geretourneerd, voegt u in plaats daarvan de $lookup
naar de pijplijn:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Merk op dat er hier een beperking is in die zin dat de uitvoer van $lookup
is altijd een array. Het maakt niet uit of er slechts één gerelateerd item is of dat er meerdere als output moeten worden opgehaald. De pijplijnfase zoekt naar de waarde van het "localField"
uit het huidige document en gebruik dat om de waarden in het "foreignField"
te matchen gespecificeerd. In dit geval is het de _id
van de aggregatie $group
target naar de _id
van de buitenlandse collectie.
Aangezien de uitvoer altijd een array is, zoals vermeld, zou de meest efficiënte manier om hiermee te werken voor deze instantie zijn door simpelweg een $unwind
stap direct na de $lookup
. Dit alles zorgt ervoor dat er een nieuw document wordt geretourneerd voor elk item dat in de doelarray wordt geretourneerd, en in dit geval verwacht u dat het er een is. In het geval dat de _id
niet overeenkomt in de buitenlandse collectie, worden de resultaten zonder overeenkomsten verwijderd.
Even een kleine opmerking:dit is eigenlijk een geoptimaliseerd patroon zoals beschreven in $lookup + $unwind Coalescence
binnen de kerndocumentatie. Hier gebeurt iets speciaals waar de $unwind
instructie is feitelijk samengevoegd in de $lookup
op een efficiënte manier te opereren. Daar kun je meer over lezen.
Bevolken gebruiken
Uit de bovenstaande inhoud zou je in principe moeten kunnen begrijpen waarom populate()
hier is het verkeerd om te doen. Afgezien van het fundamentele feit dat de uitvoer niet langer bestaat uit Warranty
modelobjecten, dat model kent eigenlijk alleen vreemde items die worden beschreven op de _accountId
eigenschap die toch niet in de uitvoer bestaat.
Nu kunt u daadwerkelijk een model definiëren dat kan worden gebruikt om de uitvoerobjecten expliciet in een gedefinieerd uitvoertype te gieten. Een korte demonstratie hiervan houdt in dat u hiervoor code aan uw toepassing toevoegt, zoals:
// 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');
Deze nieuwe Output
model kan vervolgens worden gebruikt om de resulterende eenvoudige JavaScript-objecten in Mongoose-documenten te "casten", zodat methoden zoals Model.populate()
kan eigenlijk worden gebeld:
// 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);
Sinds Output
heeft een schema gedefinieerd dat zich bewust is van de "referentie" op de _id
veld van zijn documenten de Model.populate()
weet wat het moet doen en retourneert de artikelen.
Pas echter op, aangezien dit in feite een andere vraag genereert. dat wil zeggen:
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: {} })
Waar de eerste regel de geaggregeerde uitvoer is, en dan neemt u opnieuw contact op met de server om het gerelateerde Account
te retourneren modelinvoer.
Samenvatting
Dus dat zijn uw opties, maar het zou vrij duidelijk moeten zijn dat de moderne benadering hiervan is om $lookup
en krijg een echte "join" wat niet is wat populate()
eigenlijk doet.
Inbegrepen is een lijst als een volledige demonstratie van hoe elk van deze benaderingen in de praktijk werkt. Een artistieke licentie is hier genomen, dus de weergegeven modellen zijn mogelijk niet exact hetzelfde als wat je hebt, maar er is genoeg om de basisconcepten op een reproduceerbare manier te demonstreren:
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()
}
})()
En de volledige 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"
}
]
}
]