MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Mongoose bevölkern nach Aggregat

Sie vermissen hier also tatsächlich einige Konzepte, wenn Sie ein Aggregationsergebnis "auffüllen" möchten. Normalerweise ist dies nicht das, was Sie tatsächlich tun, sondern um die Punkte zu erklären:

  1. Die Ausgabe von aggregate() ist anders als ein Model.find() oder ähnliche Maßnahmen, da der Zweck hier darin besteht, "die Ergebnisse umzugestalten". Dies bedeutet im Grunde, dass das Modell, das Sie als Quelle der Aggregation verwenden, bei der Ausgabe nicht mehr als dieses Modell betrachtet wird. Dies gilt sogar dann, wenn Sie bei der Ausgabe immer noch die exakt gleiche Dokumentstruktur beibehalten haben, sich die Ausgabe in Ihrem Fall aber ohnehin deutlich vom Quelldokument unterscheidet.

    Jedenfalls ist es keine Instanz der Warranty mehr Modell, von dem Sie beziehen, sondern nur ein einfaches Objekt. Wir können das umgehen, wenn wir später darauf eingehen.

  2. Wahrscheinlich ist der Hauptpunkt hier, dass populate() ist etwas "alter Hut" ohnehin. Dies ist wirklich nur eine Komfortfunktion, die Mongoose in den frühen Tagen der Implementierung hinzugefügt wurde. Alles, was es wirklich tut, ist eine "andere Abfrage" auf dem verwandten auszuführen Daten in einer separaten Sammlung und führt dann die Ergebnisse im Speicher mit der ursprünglichen Sammlungsausgabe zusammen.

    Aus vielen Gründen ist das in den meisten Fällen nicht wirklich effizient oder sogar wünschenswert. Und entgegen dem weit verbreiteten Irrglauben ist dies NICHT eigentlich ein "Join".

    Für einen echten "Join" verwenden Sie tatsächlich den $lookup Aggregation-Pipeline-Phase, die MongoDB verwendet, um die übereinstimmenden Elemente aus einer anderen Sammlung zurückzugeben. Im Gegensatz zu populate() Dies geschieht tatsächlich in einer einzigen Anfrage an den Server mit einer einzigen Antwort. Dies vermeidet Netzwerk-Overheads, ist im Allgemeinen schneller und erlaubt Ihnen als "echter Join" Dinge zu tun, die populate() kann nicht.

Verwenden Sie stattdessen $lookup

Die ganz schnelle Version dessen, was hier fehlt, ist, dass anstatt zu versuchen populate() in .then() Nachdem das Ergebnis zurückgegeben wurde, fügen Sie stattdessen den $lookup zur Pipeline:

  { "$lookup": {
    "from": Account.collection.name,
    "localField": "_id",
    "foreignField": "_id",
    "as": "accounts"
  }},
  { "$unwind": "$accounts" },
  { "$project": {
    "_id": "$accounts",
    "total": 1,
    "lineItems": 1
  }}

Beachten Sie, dass es hier eine Einschränkung gibt, da die Ausgabe von $lookup ist immer eine Anordnung. Es spielt keine Rolle, ob es nur ein verwandtes Element oder viele gibt, die als Ausgabe abgerufen werden sollen. Die Pipelinestufe sucht nach dem Wert von "localField" aus dem aktuell präsentierten Dokument und verwenden Sie diese, um Werte im "foreignField" abzugleichen spezifizierten. In diesem Fall ist es die _id aus der Aggregation $group Ziel auf die _id der Auslandssammlung.

Da die Ausgabe immer ein Array ist Wie bereits erwähnt, wäre der effizienteste Weg, damit für diese Instanz zu arbeiten, einfach ein $unwind Stufe direkt nach $lookup . All dies wird ein neues Dokument für jedes im Zielarray zurückgegebene Element zurückgeben, und in diesem Fall erwarten Sie, dass es eines ist. Falls die _id in der Fremdsammlung nicht abgeglichen wird, werden die Ergebnisse ohne Übereinstimmungen entfernt.

Als kleine Anmerkung:Dies ist eigentlich ein optimiertes Muster, wie in $lookup + $unwind Coalescence innerhalb der Kerndokumentation. Hier passiert etwas Besonderes, wenn der $unwind Anweisung wird tatsächlich in $lookup eingebunden Betrieb auf effiziente Weise. Mehr dazu kannst du dort nachlesen.

Bevölkerung verwenden

Aus dem obigen Inhalt sollten Sie im Grunde verstehen können, warum populate() hier ist das falsche zu tun. Abgesehen von der grundlegenden Tatsache, dass die Ausgabe nicht mehr aus Warranty besteht model-Objekte, dieses Model kennt wirklich nur fremde Elemente, die auf der _accountId beschrieben sind -Eigenschaft, die in der Ausgabe sowieso nicht vorhanden ist.

Jetzt können Sie tatsächlich ein Modell definieren, das verwendet werden kann, um die Ausgabeobjekte explizit in einen definierten Ausgabetyp umzuwandeln. Eine kurze Demonstration eines solchen Beispiels würde das Hinzufügen von Code zu Ihrer Anwendung beinhalten, wie z. 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');

Diese neue Output model kann dann verwendet werden, um die resultierenden einfachen JavaScript-Objekte in Mongoose-Dokumente zu "casten", sodass Methoden wie Model.populate() eigentlich heißen kann:

// 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);

Seit Output hat ein Schema definiert, das die "Referenz" auf _id kennt Feld davon dokumentiert die Model.populate() weiß, was zu tun ist, und gibt die Artikel zurück.

Passen Sie jedoch auf, da dies tatsächlich eine weitere Abfrage generiert. d.h.:

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: {} })

Wobei die erste Zeile die aggregierte Ausgabe ist und Sie dann den Server erneut kontaktieren, um das zugehörige Account zurückzugeben Modelleinträge.

Zusammenfassung

Das sind also Ihre Optionen, aber es sollte ziemlich klar sein, dass der moderne Ansatz dafür darin besteht, stattdessen $lookup und erhalten Sie einen echten "Join" was nicht populate() ist tatsächlich tut.

Enthalten ist eine Auflistung als vollständige Demonstration, wie jeder dieser Ansätze tatsächlich in der Praxis funktioniert. Irgendeine künstlerische Lizenz ist hier genommen, daher sind die dargestellten Modelle möglicherweise nicht exakt das gleiche wie das, was Sie haben, aber es gibt genug, um die grundlegenden Konzepte auf reproduzierbare Weise zu demonstrieren:

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()
  }

})()

Und die vollständige Ausgabe:

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"
      }
    ]
  }
]