MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Mongoose Попълване след агрегат

Така че всъщност пропускате някои понятия тук, когато поискате „попълване“ на резултат от агрегиране. Обикновено това не е това, което всъщност правите, а за да обясните точките:

  1. Резултатът от aggregate() е различно от Model.find() или подобно действие, тъй като целта тук е да се „преоформят резултатите“. Това основно означава, че моделът, който използвате като източник на агрегацията, вече не се счита за този модел на изхода. Това е вярно дори ако все още сте поддържали точно същата структура на документа при извеждане, но във вашия случай изходът така или иначе е ясно различен от изходния документ.

    Във всеки случай това вече не е екземпляр от Гаранцията модел, от който се снабдявате, но просто обикновен обект. Можем да заобиколим това, като се спрем на по-късно.

  2. Вероятно основният момент тук е, че populate() е донякъде "стара шапка" така или иначе. Това наистина е просто удобна функция, добавена към Mongoose още в първите дни на внедряването. Всичко, което наистина прави, е да изпълни „друга заявка“ за свързаните данни в отделна колекция и след това обединява резултатите в паметта към оригиналния изход на колекцията.

    По много причини това не е наистина ефективно или дори желателно в повечето случаи. И противно на популярното погрешно схващане, това еНЕ всъщност „присъединяване“.

    За истинско „присъединяване“ всъщност използвате $lookup етап на тръбопровод за агрегиране, който MongoDB използва, за да върне съответстващите елементи от друга колекция. За разлика от populate() това всъщност се прави в една заявка към сървъра с един отговор. Това избягва натоварването на мрежата, като цяло е по-бързо и като „истинско присъединяване“ ви позволява да правите неща, които populate() не мога да направя.

Вместо това използвайте $lookup

Много бързо версия на това, което липсва тук, е, че вместо опит за populate() в .then() след като резултатът бъде върнат, това, което правите вместо това, е да добавите $lookup към тръбопровода:

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

Имайте предвид, че тук има ограничение в това, че изходът на $ търсене е винаги масив. Няма значение дали има само един свързан елемент или много, които да бъдат извлечени като изход. Етапът на тръбопровода ще търси стойност на "localField" от текущия представен документ и го използвайте за съпоставяне на стойности в "foreignField" посочени. В този случай това е _id от агрегата $group насочете към _id на чуждестранната колекция.

Тъй като изходът е винаги масив както споменахме, най-ефективният начин за работа с това за този случай би бил просто да добавите $unwind етап непосредствено след $lookup . Всичко това ще направи така, че да върне нов документ за всеки елемент, върнат в целевия масив, и в този случай вие очаквате да бъде един. В случай, че _id не съответства в чуждестранната колекция, резултатите без съвпадения ще бъдат премахнати.

Като малка бележка, това всъщност е оптимизиран модел, както е описано в $lookup + $unwind Coalescence в рамките на основната документация. Специално нещо се случва тук, когато $unwind инструкцията всъщност се обединява в $lookup работа по ефективен начин. Можете да прочетете повече за това там.

Използване на попълване

От горното съдържание трябва да можете да разберете основно защо populate() тук е грешното нещо, което трябва да направите. Освен основния факт, че резултатът вече не се състои от Гаранция моделни обекти, този модел наистина знае само за чужди елементи, описани в _accountId свойство, което така или иначе не съществува в изхода.

Сега можете всъщност дефинират модел, който може да се използва за изрично преобразуване на изходните обекти в дефиниран изходен тип. Кратка демонстрация на такъв би включвала добавяне на код към вашето приложение за това като:

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

Този нов Изход след това моделът може да се използва, за да се „прехвърлят“ получените обикновени JavaScript обекти в Mongoose Documents, така че методи като Model.populate() всъщност може да се нарече:

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

Тъй като Изход има дефинирана схема, която е наясно с "референцията" на _id полето на него документира Model.populate() е наясно какво трябва да направи и връща елементите.

Внимавайте обаче, тъй като това всъщност генерира друга заявка. т.е.:

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

Където първият ред е обобщеният изход и след това се свързвате отново със сървъра, за да върнете свързания Акаунт записи на модела.

Резюме

Така че това са вашите възможности, но трябва да е доста ясно, че съвременният подход към това е вместо това да използвате $lookup и получете истинско „присъединяване“ което не е това, което populate() всъщност прави.

Включен е списък като пълна демонстрация на това как всеки от тези подходи действително работи на практика. Някакъв художествен лиценз е взето тук, така че представените модели може да не са точно същото като това, което имате, но има достатъчно, за да демонстрирате основните концепции по възпроизводим начин:

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

})()

И пълният резултат:

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. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB 4.x Синхронизиране в реално време с ElasticSearch 6.x +

  2. Възможно ли е да въвеждате прехвърлени данни в тръбопровод за агрегиране на MongoDB?

  3. Сума на агрегиране в Spring Data MongoDB

  4. Приложение, подобно на Twitter, използващо MongoDB

  5. Как да получите документа с максимална стойност за поле с map-reduce в pymongo?