sql >> Database >  >> NoSQL >> MongoDB

Mangoeste bevolken na aggregaat

U mist dus eigenlijk enkele concepten wanneer u vraagt ​​om een ​​aggregatieresultaat te "invullen". Meestal is dit niet wat u daadwerkelijk doet, maar om de punten uit te leggen:

  1. De uitvoer van aggregate() is anders dan een Model.find() of soortgelijke actie, aangezien het doel hier is om "de resultaten opnieuw vorm te geven". Dit betekent in feite dat het model dat u gebruikt als de bron van de aggregatie, niet langer als dat model wordt beschouwd bij de uitvoer. Dit geldt zelfs als u nog steeds exact dezelfde documentstructuur bij de uitvoer handhaaft, maar in uw geval is de uitvoer toch duidelijk anders dan het brondocument.

    Het is in ieder geval niet langer een instantie van de Warranty model waar u vandaan haalt, maar slechts een eenvoudig object. We kunnen daar omheen werken als we later terugkomen.

  2. Waarschijnlijk is het belangrijkste punt hier dat populate() is een beetje "oude hoed" hoe dan ook. Dit is eigenlijk gewoon een gemaksfunctie die in de allereerste dagen van implementatie aan Mongoose is toegevoegd. Het enige dat het echt doet, is "een andere zoekopdracht" uitvoeren op de gerelateerde gegevens in een aparte verzameling en voegt vervolgens de resultaten in het geheugen samen met de oorspronkelijke verzamelingsuitvoer.

    Om tal van redenen is dat in de meeste gevallen niet echt efficiënt of zelfs wenselijk. En in tegenstelling tot de populaire misvatting, is dit NIET eigenlijk een "join".

    Voor een echte "join" gebruik je eigenlijk de $lookup aggregatiepijplijnfase, die MongoDB gebruikt om de overeenkomende items uit een andere verzameling te retourneren. In tegenstelling tot populate() dit gebeurt eigenlijk in een enkel verzoek aan de server met een enkel antwoord. Dit vermijdt netwerkoverhead, is over het algemeen sneller en als een "echte join" kun je dingen doen die populate() kan niet.

Gebruik in plaats daarvan $lookup

De zeer snelle versie van wat hier ontbreekt, is dat in plaats van te proberen om populate() in de .then() nadat het resultaat is geretourneerd, voegt u in plaats daarvan de $lookup naar de pijplijn:

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

Merk op dat er hier een beperking is in die zin dat de uitvoer van $lookup is altijd een array. Het maakt niet uit of er slechts één gerelateerd item is of dat er meerdere als output moeten worden opgehaald. De pijplijnfase zoekt naar de waarde van het "localField" uit het huidige document en gebruik dat om de waarden in het "foreignField" te matchen gespecificeerd. In dit geval is het de _id van de aggregatie $group target naar de _id van de buitenlandse collectie.

Aangezien de uitvoer altijd een array is, zoals vermeld, zou de meest efficiënte manier om hiermee te werken voor deze instantie zijn door simpelweg een $unwind stap direct na de $lookup . Dit alles zorgt ervoor dat er een nieuw document wordt geretourneerd voor elk item dat in de doelarray wordt geretourneerd, en in dit geval verwacht u dat het er een is. In het geval dat de _id niet overeenkomt in de buitenlandse collectie, worden de resultaten zonder overeenkomsten verwijderd.

Even een kleine opmerking:dit is eigenlijk een geoptimaliseerd patroon zoals beschreven in $lookup + $unwind Coalescence binnen de kerndocumentatie. Hier gebeurt iets speciaals waar de $unwind instructie is feitelijk samengevoegd in de $lookup op een efficiënte manier te opereren. Daar kun je meer over lezen.

Bevolken gebruiken

Uit de bovenstaande inhoud zou je in principe moeten kunnen begrijpen waarom populate() hier is het verkeerd om te doen. Afgezien van het fundamentele feit dat de uitvoer niet langer bestaat uit Warranty modelobjecten, dat model kent eigenlijk alleen vreemde items die worden beschreven op de _accountId eigenschap die toch niet in de uitvoer bestaat.

Nu kunt u daadwerkelijk een model definiëren dat kan worden gebruikt om de uitvoerobjecten expliciet in een gedefinieerd uitvoertype te gieten. Een korte demonstratie hiervan houdt in dat u hiervoor code aan uw toepassing toevoegt, zoals:

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

Deze nieuwe Output model kan vervolgens worden gebruikt om de resulterende eenvoudige JavaScript-objecten in Mongoose-documenten te "casten", zodat methoden zoals Model.populate() kan eigenlijk worden gebeld:

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

Sinds Output heeft een schema gedefinieerd dat zich bewust is van de "referentie" op de _id veld van zijn documenten de Model.populate() weet wat het moet doen en retourneert de artikelen.

Pas echter op, aangezien dit in feite een andere vraag genereert. dat wil zeggen:

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

Waar de eerste regel de geaggregeerde uitvoer is, en dan neemt u opnieuw contact op met de server om het gerelateerde Account te retourneren modelinvoer.

Samenvatting

Dus dat zijn uw opties, maar het zou vrij duidelijk moeten zijn dat de moderne benadering hiervan is om $lookup en krijg een echte "join" wat niet is wat populate() eigenlijk doet.

Inbegrepen is een lijst als een volledige demonstratie van hoe elk van deze benaderingen in de praktijk werkt. Een artistieke licentie is hier genomen, dus de weergegeven modellen zijn mogelijk niet exact hetzelfde als wat je hebt, maar er is genoeg om de basisconcepten op een reproduceerbare manier te demonstreren:

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

})()

En de volledige 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. Hoe krijg ik de nieuwe waarde terug na een update in een ingesloten array?

  2. Toegang krijgen tot de localhost van de host vanuit het kubernetes-cluster

  3. Redis Async API's

  4. Retourneer laatste record van subdocument in Mongodb