sql >> Database teknologi >  >> NoSQL >> MongoDB

Mongoose Befolker efter Aggregate

Så du mangler faktisk nogle begreber her, når du beder om at "befolke" på et aggregeringsresultat. Det er typisk ikke, hvad du rent faktisk gør, men for at forklare pointerne:

  1. Outputtet af aggregate() er i modsætning til en Model.find() eller lignende handling, da formålet her er at "omforme resultaterne". Dette betyder grundlæggende, at den model, du bruger som kilde til aggregeringen, ikke længere betragtes som den model ved output. Dette er endda sandt, hvis du stadig beholdt den nøjagtige samme dokumentstruktur på output, men i dit tilfælde er output klart anderledes end kildedokumentet alligevel.

    Det er i hvert fald ikke længere en forekomst af Garanti model du henter fra, men bare en almindelig genstand. Vi kan omgå det, efterhånden som vi berører det senere.

  2. Det vigtigste her er nok, at populate() er noget "gammel hat" alligevel. Dette er egentlig bare en bekvemmelighedsfunktion, der blev tilføjet til Mongoose tilbage i de meget tidlige dage af implementering. Det eneste, det virkelig gør, er at udføre "en anden forespørgsel" på den relaterede data i en separat samling, og fletter derefter resultaterne i hukommelsen til det originale samlingsoutput.

    Af mange grunde er det i de fleste tilfælde ikke rigtig effektivt eller endda ønskeligt. Og i modsætning til den populære misforståelse er dette IKKE faktisk et "join".

    For et rigtigt "join" bruger du faktisk $lookup aggregeringspipeline-stadiet, som MongoDB bruger til at returnere de matchende varer fra en anden samling. I modsætning til populate() dette gøres faktisk i en enkelt anmodning til serveren med et enkelt svar. Dette undgår netværksomkostninger, er generelt hurtigere og giver dig som en "rigtig joinforbindelse" mulighed for at gøre ting, der befolker() ikke kan gøre.

Brug $lookup i stedet

Den meget hurtige version af det, der mangler her, er, at i stedet for at forsøge at populate() i .then() efter resultatet er returneret, hvad du i stedet gør, er at tilføje $lookup til rørledningen:

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

Bemærk, at der er en begrænsning her i, at output fra $ opslag er altid et array. Det er ligegyldigt, om der kun er én relateret vare eller mange, der skal hentes som output. Pipelinestadiet vil lede efter værdien af ​​"localField" fra det aktuelle dokument, der præsenteres, og brug det til at matche værdier i "foreignField" specificeret. I dette tilfælde er det _id fra sammenlægningen $group mål til _id af den udenlandske samling.

Da outputtet altid er et array som nævnt vil den mest effektive måde at arbejde med dette på i dette tilfælde være blot at tilføje en $unwind trin direkte efter $lookup . Alt dette vil gøre det til at returnere et nyt dokument for hver genstand, der returneres i målarrayet, og i dette tilfælde forventer du, at det er et. I det tilfælde, hvor _id ikke matches i den udenlandske samling, ville resultaterne uden match blive fjernet.

Som en lille bemærkning er dette faktisk et optimeret mønster som beskrevet i $lookup + $unwind Coalescence inden for kernedokumentationen. En særlig ting sker her, hvor $unwind instruktion er faktisk flettet ind i $lookup drift på en effektiv måde. Det kan du læse mere om der.

Bruger udfylde

Ud fra ovenstående indhold burde du grundlæggende kunne forstå hvorfor populate() her er den forkerte ting at gøre. Bortset fra det grundlæggende faktum, at output ikke længere består af Garanti modelobjekter, kender denne model egentlig kun til fremmede elementer beskrevet på _accountId egenskab, som alligevel ikke findes i outputtet.

Nu kan faktisk definere en model, som kan bruges til eksplicit at caste output-objekterne til en defineret outputtype. En kort demonstration af en ville indebære at tilføje kode til din ansøgning til dette som:

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

Denne nye Output model kan derefter bruges til at "caste" de resulterende almindelige JavaScript-objekter ind i Mongoose Documents, så metoder som Model.populate() kan faktisk kaldes:

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

Siden Output har et skema defineret, der er opmærksom på "referencen" på _id feltet i dets dokumenter Model.populate() er klar over, hvad den skal gøre og returnerer varerne.

Pas dog på, da dette faktisk genererer en anden forespørgsel. dvs.:

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

Hvor den første linje er det samlede output, og derefter kontakter du serveren igen for at returnere den relaterede Konto modelindtastninger.

Oversigt

Så det er dine muligheder, men det burde være ret klart, at den moderne tilgang til dette i stedet er at bruge $lookup og få et rigtigt "join" hvilket ikke er det populate() faktisk gør.

Inkluderet er en liste som en fuldstændig demonstration af, hvordan hver af disse tilgange faktisk fungerer i praksis. Nogle kunstneriske licenser er taget her, så de repræsenterede modeller er muligvis ikke præcis det samme som du har, men der er nok til at demonstrere de grundlæggende begreber på en reproducerbar måde:

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

})()

Og det fulde output:

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


  1. MongoDB kan ikke opdatere dokumentet, fordi _id er streng, ikke ObjectId

  2. Mongodb finde et dokument med alle underdokumenter, der opfylder en betingelse

  3. REST API-kald virker kun én gang

  4. MongoDB-dokumenter udløber for tidligt (mongoose)