Sie vermissen hier also tatsächlich einige Konzepte, wenn Sie ein Aggregationsergebnis "auffüllen" möchten. Normalerweise ist dies nicht das, was Sie tatsächlich tun, sondern um die Punkte zu erklären:
-
Die Ausgabe von
aggregate()
ist anders als einModel.find()
oder ähnliche Maßnahmen, da der Zweck hier darin besteht, "die Ergebnisse umzugestalten". Dies bedeutet im Grunde, dass das Modell, das Sie als Quelle der Aggregation verwenden, bei der Ausgabe nicht mehr als dieses Modell betrachtet wird. Dies gilt sogar dann, wenn Sie bei der Ausgabe immer noch die exakt gleiche Dokumentstruktur beibehalten haben, sich die Ausgabe in Ihrem Fall aber ohnehin deutlich vom Quelldokument unterscheidet.Jedenfalls ist es keine Instanz der
Warranty
mehr Modell, von dem Sie beziehen, sondern nur ein einfaches Objekt. Wir können das umgehen, wenn wir später darauf eingehen. -
Wahrscheinlich ist der Hauptpunkt hier, dass
populate()
ist etwas "alter Hut" ohnehin. Dies ist wirklich nur eine Komfortfunktion, die Mongoose in den frühen Tagen der Implementierung hinzugefügt wurde. Alles, was es wirklich tut, ist eine "andere Abfrage" auf dem verwandten auszuführen Daten in einer separaten Sammlung und führt dann die Ergebnisse im Speicher mit der ursprünglichen Sammlungsausgabe zusammen.Aus vielen Gründen ist das in den meisten Fällen nicht wirklich effizient oder sogar wünschenswert. Und entgegen dem weit verbreiteten Irrglauben ist dies NICHT eigentlich ein "Join".
Für einen echten "Join" verwenden Sie tatsächlich den
$lookup
Aggregation-Pipeline-Phase, die MongoDB verwendet, um die übereinstimmenden Elemente aus einer anderen Sammlung zurückzugeben. Im Gegensatz zupopulate()
Dies geschieht tatsächlich in einer einzigen Anfrage an den Server mit einer einzigen Antwort. Dies vermeidet Netzwerk-Overheads, ist im Allgemeinen schneller und erlaubt Ihnen als "echter Join" Dinge zu tun, diepopulate()
kann nicht.
Verwenden Sie stattdessen $lookup
Die ganz schnelle Version dessen, was hier fehlt, ist, dass anstatt zu versuchen populate()
in .then()
Nachdem das Ergebnis zurückgegeben wurde, fügen Sie stattdessen den $lookup
zur Pipeline:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Beachten Sie, dass es hier eine Einschränkung gibt, da die Ausgabe von $lookup
ist immer eine Anordnung. Es spielt keine Rolle, ob es nur ein verwandtes Element oder viele gibt, die als Ausgabe abgerufen werden sollen. Die Pipelinestufe sucht nach dem Wert von "localField"
aus dem aktuell präsentierten Dokument und verwenden Sie diese, um Werte im "foreignField"
abzugleichen spezifizierten. In diesem Fall ist es die _id
aus der Aggregation $group
Ziel auf die _id
der Auslandssammlung.
Da die Ausgabe immer ein Array ist Wie bereits erwähnt, wäre der effizienteste Weg, damit für diese Instanz zu arbeiten, einfach ein $unwind
Stufe direkt nach $lookup
. All dies wird ein neues Dokument für jedes im Zielarray zurückgegebene Element zurückgeben, und in diesem Fall erwarten Sie, dass es eines ist. Falls die _id
in der Fremdsammlung nicht abgeglichen wird, werden die Ergebnisse ohne Übereinstimmungen entfernt.
Als kleine Anmerkung:Dies ist eigentlich ein optimiertes Muster, wie in $lookup + $unwind Coalescence
innerhalb der Kerndokumentation. Hier passiert etwas Besonderes, wenn der $unwind
Anweisung wird tatsächlich in $lookup
eingebunden
Betrieb auf effiziente Weise. Mehr dazu kannst du dort nachlesen.
Bevölkerung verwenden
Aus dem obigen Inhalt sollten Sie im Grunde verstehen können, warum populate()
hier ist das falsche zu tun. Abgesehen von der grundlegenden Tatsache, dass die Ausgabe nicht mehr aus Warranty
besteht model-Objekte, dieses Model kennt wirklich nur fremde Elemente, die auf der _accountId
beschrieben sind -Eigenschaft, die in der Ausgabe sowieso nicht vorhanden ist.
Jetzt können Sie tatsächlich ein Modell definieren, das verwendet werden kann, um die Ausgabeobjekte explizit in einen definierten Ausgabetyp umzuwandeln. Eine kurze Demonstration eines solchen Beispiels würde das Hinzufügen von Code zu Ihrer Anwendung beinhalten, wie z. B.:
// 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');
Diese neue Output
model kann dann verwendet werden, um die resultierenden einfachen JavaScript-Objekte in Mongoose-Dokumente zu "casten", sodass Methoden wie Model.populate()
eigentlich heißen kann:
// 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);
Seit Output
hat ein Schema definiert, das die "Referenz" auf _id
kennt Feld davon dokumentiert die Model.populate()
weiß, was zu tun ist, und gibt die Artikel zurück.
Passen Sie jedoch auf, da dies tatsächlich eine weitere Abfrage generiert. d.h.:
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: {} })
Wobei die erste Zeile die aggregierte Ausgabe ist und Sie dann den Server erneut kontaktieren, um das zugehörige Account
zurückzugeben Modelleinträge.
Zusammenfassung
Das sind also Ihre Optionen, aber es sollte ziemlich klar sein, dass der moderne Ansatz dafür darin besteht, stattdessen $lookup
und erhalten Sie einen echten "Join" was nicht populate()
ist tatsächlich tut.
Enthalten ist eine Auflistung als vollständige Demonstration, wie jeder dieser Ansätze tatsächlich in der Praxis funktioniert. Irgendeine künstlerische Lizenz ist hier genommen, daher sind die dargestellten Modelle möglicherweise nicht exakt das gleiche wie das, was Sie haben, aber es gibt genug, um die grundlegenden Konzepte auf reproduzierbare Weise zu demonstrieren:
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()
}
})()
Und die vollständige Ausgabe:
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"
}
]
}
]