Takže vám zde ve skutečnosti chybí některé pojmy, když žádáte o „vyplnění“ výsledku agregace. Obvykle to není to, co ve skutečnosti děláte, ale vysvětlení bodů:
-
Výstup
aggregate()
je na rozdíl odModel.find()
nebo podobnou akci, protože účelem je zde „přetvořit výsledky“. To v podstatě znamená, že model, který používáte jako zdroj agregace, již není na výstupu považován za tento model. To platí i v případě, že jste na výstupu stále zachovali přesně stejnou strukturu dokumentu, ale ve vašem případě se výstup stejně jasně liší od zdrojového dokumentu.V každém případě se již nejedná o instanci
Warranty
model, ze kterého získáváte zdroje, ale pouze prostý objekt. Můžeme to obejít, když se toho dotkneme později. -
Pravděpodobně hlavním bodem je, že
populate()
je poněkud "starý klobouk" tak jako tak. Toto je opravdu jen funkce pohodlí přidaná do Mongoose ve velmi raných dnech implementace. Jediné, co skutečně dělá, je provést "další dotaz" na související data v samostatné kolekci a poté sloučí výsledky v paměti s výstupem původní kolekce.Z mnoha důvodů to není ve většině případů skutečně efektivní nebo dokonce žádoucí. A na rozdíl od populární mylné představy to NE vlastně „připojení“.
Pro skutečné „připojení“ ve skutečnosti používáte
$lookup
fázi agregačního kanálu, kterou MongoDB používá k vrácení odpovídajících položek z jiné kolekce. Na rozdíl odpopulate()
to se ve skutečnosti provádí v jediném požadavku na server s jedinou odpovědí. Tím se vyhnete režii sítě, je obecně rychlejší a jako "skutečné spojení" vám umožní dělat věci, kterépopulate()
nemůže udělat.
Použijte místo toho $lookup
Velmi rychlé verze toho, co zde chybí, je, že namísto pokusu o populate()
v .then()
po vrácení výsledku místo toho přidáte $lookup
do potrubí:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Všimněte si, že zde existuje omezení v tom, že výstup $lookup
je vždy pole. Nezáleží na tom, zda existuje pouze jedna související položka nebo mnoho, které mají být načteny jako výstup. Fáze kanálu bude hledat hodnotu "localField"
z aktuálního prezentovaného dokumentu a použijte jej ke spárování hodnot v "foreignField"
specifikováno. V tomto případě je to _id
z agregace $group
cílit na _id
zahraniční sbírky.
Protože výstup je vždy pole jak bylo zmíněno, nejúčinnějším způsobem, jak s tím v této instanci pracovat, by bylo jednoduše přidat $unwind
fázi přímo navazující na $lookup
. To vše vrátí nový dokument pro každou položku vrácenou v cílovém poli a v tomto případě očekáváte, že to bude jeden. V případě, že _id
neodpovídá v zahraniční kolekci, výsledky bez shody budou odstraněny.
Malá poznámka, toto je ve skutečnosti optimalizovaný vzor, jak je popsáno v $lookup + $unwind Coalescence
v rámci základní dokumentace. Zde se stane zvláštní věc, když $unwind
instrukce je ve skutečnosti sloučena do $lookup
provozovat efektivním způsobem. Zde si o tom můžete přečíst více.
Pomocí naplnění
Z výše uvedeného obsahu byste měli být schopni v podstatě pochopit, proč populate()
tady je špatná věc. Kromě základního faktu, že výstup již neobsahuje Warranty
modelové objekty, tento model ve skutečnosti zná pouze cizí položky popsané v _accountId
vlastnost, která stejně ve výstupu neexistuje.
Nyní můžete skutečně definovat model, který lze použít k explicitnímu přetypování výstupních objektů do definovaného výstupního typu. Krátká ukázka jednoho by zahrnovala přidání kódu do vaší aplikace, například:
// 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');
Tento nový Output
model lze poté použít k „osazení“ výsledných prostých objektů JavaScriptu do dokumentů Mongoose, takže metody jako Model.populate()
lze ve skutečnosti nazvat:
// 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);
Od Output
má definované schéma, které si je vědomo "odkazu" na _id
pole jeho dokumentů Model.populate()
ví, co musí udělat, a vrátí položky.
Dejte si však pozor, protože to ve skutečnosti generuje další dotaz. tj.:
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: {} })
Kde první řádek je souhrnný výstup a poté znovu kontaktujete server, abyste vrátili související Account
položky modelu.
Shrnutí
Takže toto jsou vaše možnosti, ale mělo by být docela jasné, že moderní přístup k tomu je místo toho použít $lookup
a získejte skutečné „připojení“ což není to, co populate()
skutečně dělá.
Součástí je seznam jako úplná ukázka toho, jak každý z těchto přístupů skutečně funguje v praxi. Nějaká umělecká licence je převzato zde, takže zastoupené modely nemusí být přesně stejné jako to, co máte, ale je toho dost na to, aby bylo možné demonstrovat základní pojmy reprodukovatelným způsobem:
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()
}
})()
A úplný výstup:
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"
}
]
}
]