MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Mangouste peupler après agrégat

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 :

  1. La sortie de aggregate() est différent d'un Model.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.

  2. 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 qui populate() 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"
      }
    ]
  }
]