Entonces, en realidad, le faltan algunos conceptos aquí cuando solicita "rellenar" en un resultado de agregación. Por lo general, esto no es lo que realmente hace, pero para explicar los puntos:
-
La salida de
aggregate()
es diferente a unModel.find()
o una acción similar ya que el propósito aquí es "remodelar los resultados". Básicamente, esto significa que el modelo que está utilizando como fuente de la agregación ya no se considera ese modelo en la salida. Esto es cierto incluso si mantuvo exactamente la misma estructura del documento en la salida, pero en su caso, la salida es claramente diferente al documento de origen de todos modos.En cualquier caso, ya no es una instancia de la
Warranty
modelo del que proviene, pero solo un objeto simple. Podemos solucionar eso a medida que lo tratemos más adelante. -
Probablemente el punto principal aquí es que
populate()
es algo "sombrero viejo" de todos modos. Esta es realmente solo una función de conveniencia agregada a Mongoose en los primeros días de implementación. Todo lo que realmente hace es ejecutar "otra consulta" en el relacionado datos en una colección separada y luego combina los resultados en la memoria con la salida de la colección original.Por muchas razones, eso no es realmente eficiente o incluso deseable en la mayoría de los casos. Y contrariamente a la idea errónea popular, esto NO en realidad una "unión".
Para una "unión" real, en realidad usa el
$lookup
Etapa de canalización de agregación, que MongoDB usa para devolver los elementos coincidentes de otra colección. A diferencia depopulate()
esto se hace en realidad en una sola solicitud al servidor con una sola respuesta. Esto evita los gastos generales de la red, generalmente es más rápido y como una "unión real" le permite hacer cosas quepopulate()
no puede hacer.
Use $búsqueda en su lugar
El muy rápido la versión de lo que falta aquí es que en lugar de intentar populate()
en el .then()
después de que se devuelva el resultado, lo que debe hacer es agregar el $lookup
a la canalización:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Tenga en cuenta que hay una restricción aquí en la salida de $lookup
es siempre una matriz. No importa si solo hay un elemento relacionado o muchos para obtener como salida. La etapa de canalización buscará el valor de "localField"
del documento actual presentado y utilícelo para hacer coincidir los valores en el "foreignField"
especificado. En este caso es el _id
de la agregación $group
destino al _id
de la colección extranjera.
Dado que la salida es siempre una matriz como se mencionó, la forma más eficiente de trabajar con esto para esta instancia sería simplemente agregar un $unwind
etapa directamente siguiendo el $lookup
. Todo esto hará que devuelva un nuevo documento para cada elemento devuelto en la matriz de destino, y en este caso espera que sea uno. En el caso de que el _id
no coincide en la colección extranjera, los resultados sin coincidencias se eliminarán.
Como pequeña nota, este es en realidad un patrón optimizado como se describe en $buscar + $desenrollar Coalescence
dentro de la documentación central. Algo especial sucede aquí donde $unwind
la instrucción en realidad se fusiona con $lookup
operación de una manera eficiente. Puede leer más sobre eso allí.
Usando poblar
A partir del contenido anterior, debería poder comprender básicamente por qué populate()
aquí está lo incorrecto que hacer. Aparte del hecho básico de que la salida ya no se compone de Warranty
objetos del modelo, ese modelo realmente solo conoce los elementos extraños descritos en el _accountId
propiedad que no existe en la salida de todos modos.
Ahora usted puede defina realmente un modelo que se pueda usar para convertir explícitamente los objetos de salida en un tipo de salida definido. Una breve demostración de uno implicaría agregar código a su aplicación para esto como:
// 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');
Esta nueva Output
El modelo se puede usar para "transmitir" los objetos JavaScript sin formato resultantes en documentos Mongoose para que métodos como Model.populate()
en realidad se puede llamar:
// 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);
Desde Output
tiene un esquema definido que es consciente de la "referencia" en el _id
campo de sus documentos el Model.populate()
sabe lo que tiene que hacer y devuelve los artículos.
Sin embargo, tenga cuidado, ya que esto genera otra consulta. es decir:
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: {} })
Donde la primera línea es la salida agregada, y luego está contactando al servidor nuevamente para devolver la Account
relacionada entradas del modelo.
Resumen
Esas son sus opciones, pero debería quedar bastante claro que el enfoque moderno para esto es usar $lookup
y obtén una "unión" real que no es lo que populate()
está haciendo realmente.
Se incluye una lista como demostración completa de cómo funciona cada uno de estos enfoques en la práctica. Alguna licencia artística se toma aquí, por lo que los modelos representados pueden no ser exactamente lo mismo que tiene, pero hay suficiente para demostrar los conceptos básicos de una manera reproducible:
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()
}
})()
Y la salida completa:
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"
}
]
}
]