Il vous manque donc certains concepts ici lorsque vous demandez à "remplir" un résultat d'agrégation. En règle générale, ce n'est pas ce que vous faites réellement, mais pour expliquer les points :
-
La sortie de
aggregate()
est différent d'unModel.find()
ou une action similaire puisque le but ici est de "remodeler les résultats". Cela signifie essentiellement que le modèle que vous utilisez comme source de l'agrégation n'est plus considéré comme ce modèle en sortie. Cela est même vrai si vous avez conservé exactement la même structure de document en sortie, mais dans votre cas, la sortie est de toute façon clairement différente du document source.En tout cas, ce n'est plus une instance de la
Warranty
modèle dont vous vous approvisionnez, mais juste un objet ordinaire. Nous pouvons contourner ce problème lorsque nous y reviendrons plus tard. -
Le point principal ici est probablement que
populate()
est un peu "vieux chapeau" De toute façon. Il ne s'agit en réalité que d'une fonction pratique ajoutée à Mongoose au tout début de sa mise en œuvre. Tout ce qu'il fait vraiment est d'exécuter "une autre requête" sur le lié données dans une collection distincte, puis fusionne les résultats en mémoire avec la sortie de la collection d'origine.Pour de nombreuses raisons, ce n'est pas vraiment efficace ni même souhaitable dans la plupart des cas. Et contrairement à l'idée fausse populaire, ce n'est PAS en fait une "jointure".
Pour une véritable « jointure », vous utilisez en fait le
$lookup
étape de pipeline d'agrégation, que MongoDB utilise pour renvoyer les éléments correspondants d'une autre collection. Contrairement àpopulate()
cela se fait en fait en une seule requête au serveur avec une seule réponse. Cela évite les frais généraux du réseau, est généralement plus rapide et comme une "vraie jointure" vous permet de faire des choses quipopulate()
ne peut pas faire.
Utilisez $lookup à la place
Le très rapide version de ce qui manque ici est qu'au lieu d'essayer de populate()
dans le .then()
une fois le résultat renvoyé, ce que vous faites à la place est d'ajouter le $lookup
au pipeline :
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Notez qu'il y a une contrainte ici dans la mesure où la sortie de $lookup
est toujours un tableau. Peu importe s'il n'y a qu'un seul élément connexe ou plusieurs à récupérer en sortie. L'étape du pipeline recherchera la valeur du "localField"
à partir du document actuel présenté et utilisez-le pour faire correspondre les valeurs dans le "foreignField"
spécifié. Dans ce cas c'est le _id
de l'agrégation $group
cible au _id
de la collection étrangère.
Puisque la sortie est toujours un tableau comme mentionné, le moyen le plus efficace de travailler avec ceci pour cette instance serait simplement d'ajouter un $unwind
étape suivant directement le $lookup
. Tout cela va le faire renvoyer un nouveau document pour chaque élément renvoyé dans le tableau cible, et dans ce cas, vous vous attendez à ce qu'il en soit un. Dans le cas où le _id
n'est pas mis en correspondance dans la collection étrangère, les résultats sans correspondance seront supprimés.
Comme petite note, il s'agit en fait d'un modèle optimisé comme décrit dans $lookup + $unwind Coalescence
dans la documentation de base. Une chose spéciale se produit ici où le $unwind
l'instruction est en fait fusionnée dans le $lookup
fonctionnement de manière efficace. Vous pouvez en savoir plus à ce sujet ici.
Utiliser le remplissage
À partir du contenu ci-dessus, vous devriez être en mesure de comprendre pourquoi populate()
voici la mauvaise chose à faire. Mis à part le fait fondamental que la sortie n'est plus composée de Warranty
objets de modèle, ce modèle ne connaît vraiment que les éléments étrangers décrits sur le _accountId
propriété qui n'existe de toute façon pas dans la sortie.
Maintenant, vous pouvez définissent en fait un modèle qui peut être utilisé pour convertir explicitement les objets de sortie en un type de sortie défini. Une courte démonstration de l'un impliquerait d'ajouter du code à votre application pour cela comme :
// 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');
Cette nouvelle Output
modèle peut ensuite être utilisé pour "caster" les objets JavaScript bruts résultants dans des documents Mongoose afin que des méthodes telles que Model.populate()
peut en fait s'appeler :
// 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);
Depuis Output
a un schéma défini qui est conscient de la "référence" sur le _id
champ de ses documents le Model.populate()
est conscient de ce qu'il doit faire et renvoie les éléments.
Attention cependant car cela génère en fait une autre requête. c'est-à-dire :
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: {} })
Où la première ligne est la sortie agrégée, puis vous recontactez le serveur afin de renvoyer le Account
associé entrées de modèle.
Résumé
Ce sont donc vos options, mais il devrait être assez clair que l'approche moderne consiste à utiliser $lookup
et obtenez une véritable "adhésion" ce qui n'est pas ce que populate()
est en train de faire.
Inclus est une liste comme une démonstration complète de la façon dont chacune de ces approches fonctionne réellement dans la pratique. Une licence artistique est pris ici, donc les modèles représentés peuvent ne pas être exactement identique à ce que vous avez, mais il y en a assez pour démontrer les concepts de base de manière reproductible :
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()
}
})()
Et la sortie complète :
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"
}
]
}
]