Tak więc brakuje tu niektórych pojęć, gdy prosisz o „wypełnienie” wyniku agregacji. Zazwyczaj nie jest to to, co faktycznie robisz, ale wyjaśnienie punktów:
-
Wyjście
aggregate()różni się odModel.find()lub podobne działanie, ponieważ celem jest „przekształcenie wyników”. Zasadniczo oznacza to, że model używany jako źródło agregacji nie jest już uważany za model na wyjściu. Dzieje się tak nawet wtedy, gdy nadal zachowujesz dokładnie tę samą strukturę dokumentu na wyjściu, ale w Twoim przypadku wynik i tak wyraźnie różni się od dokumentu źródłowego.W każdym razie nie jest to już instancja
Gwarancjimodel, z którego czerpiesz, ale tylko zwykły obiekt. Możemy to obejść, gdy porozmawiamy później. -
Prawdopodobnie głównym punktem tutaj jest to, że
populate()jest trochę "stary kapelusz" w każdym razie. Jest to tak naprawdę tylko wygodna funkcja dodana do Mongoose w bardzo wczesnych dniach wdrażania. Wszystko, co tak naprawdę robi, to wykonanie „kolejnego zapytania” na powiązanym dane w oddzielnej kolekcji, a następnie scala wyniki w pamięci z oryginalnym wyjściem kolekcji.Z wielu powodów nie jest to zbyt wydajne, a nawet pożądane w większości przypadków. I w przeciwieństwie do popularnego błędnego przekonania, NIE właściwie „dołączenie”.
Do prawdziwego „dołączenia” używasz
$lookupetap potoku agregacji, którego MongoDB używa do zwracania pasujących elementów z innej kolekcji. W przeciwieństwie dopopulate()w rzeczywistości odbywa się to w pojedynczym żądaniu do serwera z pojedynczą odpowiedzią. Pozwala to uniknąć narzutów sieciowych, jest generalnie szybsze i jako „prawdziwe dołączenie” pozwala robić rzeczy, którewypełniają()nie mogę tego zrobić.
Zamiast tego użyj wyszukiwania $
Bardzo szybkie brakującą wersją jest to, że zamiast próbować wypełniać() w .then() po zwróceniu wyniku zamiast tego dodajesz $oglądaj
do rurociągu:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Zauważ, że istnieje tutaj ograniczenie, ponieważ wyjście $ wyszukiwanie
jest zawsze tablica. Nie ma znaczenia, czy istnieje tylko jeden powiązany element, czy wiele do pobrania jako dane wyjściowe. Etap potoku będzie szukał wartości "localField" z bieżącego prezentowanego dokumentu i użyj go do dopasowania wartości w "foreignField" określony. W tym przypadku jest to _id z agregacji $group
cel na _id kolekcji zagranicznej.
Ponieważ wyjście jest zawsze tablicą jak wspomniano, najskuteczniejszym sposobem pracy z tym w tym przypadku byłoby po prostu dodanie $unwind
etap bezpośrednio po $lookup
. Wszystko to spowoduje zwrócenie nowego dokumentu dla każdego elementu zwróconego w tablicy docelowej, aw tym przypadku oczekujesz, że będzie to jeden. W przypadku, gdy _id nie zostanie dopasowany w kolekcji zagranicznej, wyniki bez dopasowań zostaną usunięte.
Na marginesie, jest to w rzeczywistości zoptymalizowany wzorzec opisany w $ lookup + $unwind Koalescencja
w ramach podstawowej dokumentacji. Wyjątkowa rzecz dzieje się tutaj, gdy $unwind
instrukcja jest w rzeczywistości połączona z $lookup
w efektywny sposób. Więcej na ten temat możesz przeczytać tam.
Korzystanie z wypełniania
Z powyższej treści powinieneś być w stanie zrozumieć, dlaczego populate() tutaj jest zła rzecz do zrobienia. Poza podstawowym faktem, że dane wyjściowe nie są już objęte Gwarancją obiekty modelu, ten model tak naprawdę wie tylko o obcych elementach opisanych w _accountId właściwość, która i tak nie istnieje w danych wyjściowych.
Teraz możesz faktycznie zdefiniować model, którego można użyć w celu jawnego rzutowania obiektów wyjściowych na określony typ wyjściowy. Krótka demonstracja jednego z nich wymagałaby dodania kodu do aplikacji w następujący sposó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');
To nowe Wyjście model może być następnie użyty w celu "rzucenia" wynikowych zwykłych obiektów JavaScript do Mongoose Documents, tak aby metody takie jak Model.populate()
można nazwać:
// 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 Wyjście ma zdefiniowany schemat, który jest świadomy „odniesienia” w _id pole jego dokumentów Model.populate() jest świadomy tego, co musi zrobić i zwraca przedmioty.
Uważaj jednak, ponieważ to faktycznie generuje kolejne zapytanie. czyli:
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: {} })
Gdzie pierwsza linia to zagregowane dane wyjściowe, a następnie ponownie kontaktujesz się z serwerem w celu zwrócenia powiązanego Konta wpisy modeli.
Podsumowanie
Więc to są twoje opcje, ale powinno być całkiem jasne, że nowoczesne podejście do tego polega na użyciu $lookup
i uzyskaj prawdziwe „dołączenie” co nie jest tym, co wypełnij() faktycznie robi.
Dołączona jest lista jako pełna demonstracja tego, jak każde z tych podejść faktycznie działa w praktyce. Niektóre licencje artystyczne jest tutaj wzięty, więc przedstawione modele mogą nie być dokładnie tak samo, jak masz, ale wystarczy, aby zademonstrować podstawowe pojęcia w powtarzalny sposób:
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', 'example@sqldat.com'],
['Second Account', 'Second Person', 'example@sqldat.com'],
['Third Account', 'Third Person', 'example@sqldat.com']
].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()
}
})()
I pełne wyjście:
Mongoose: dontuseme.deleteMany({}, {})
Mongoose: warranties.deleteMany({}, {})
Mongoose: accounts.deleteMany({}, {})
Mongoose: accounts.insertMany([ { _id: 5bf4b591a06509544b8cf75b, name: 'First Account', contactName: 'First Person', contactEmail: 'example@sqldat.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75c, name: 'Second Account', contactName: 'Second Person', contactEmail: 'example@sqldat.com', __v: 0 }, { _id: 5bf4b591a06509544b8cf75d, name: 'Third Account', contactName: 'Third Person', contactEmail: 'example@sqldat.com', __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": "example@sqldat.com",
"__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": "example@sqldat.com",
"__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": "example@sqldat.com",
"__v": 0
},
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
]
},
{
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "example@sqldat.com",
"__v": 0
},
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
]
}
]