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
Gwarancji
model, 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
$lookup
etap 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', '[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()
}
})()
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: '[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"
}
]
}
]