sql >> Databáze >  >> NoSQL >> MongoDB

Mongoose osídlí po agregaci

Takže vám zde ve skutečnosti chybí některé pojmy, když žádáte o „vyplnění“ výsledku agregace. Obvykle to není to, co ve skutečnosti děláte, ale vysvětlení bodů:

  1. Výstup aggregate() je na rozdíl od Model.find() nebo podobnou akci, protože účelem je zde „přetvořit výsledky“. To v podstatě znamená, že model, který používáte jako zdroj agregace, již není na výstupu považován za tento model. To platí i v případě, že jste na výstupu stále zachovali přesně stejnou strukturu dokumentu, ale ve vašem případě se výstup stejně jasně liší od zdrojového dokumentu.

    V každém případě se již nejedná o instanci Warranty model, ze kterého získáváte zdroje, ale pouze prostý objekt. Můžeme to obejít, když se toho dotkneme později.

  2. Pravděpodobně hlavním bodem je, že populate() je poněkud "starý klobouk" tak jako tak. Toto je opravdu jen funkce pohodlí přidaná do Mongoose ve velmi raných dnech implementace. Jediné, co skutečně dělá, je provést "další dotaz" na související data v samostatné kolekci a poté sloučí výsledky v paměti s výstupem původní kolekce.

    Z mnoha důvodů to není ve většině případů skutečně efektivní nebo dokonce žádoucí. A na rozdíl od populární mylné představy to NE vlastně „připojení“.

    Pro skutečné „připojení“ ve skutečnosti používáte $lookup fázi agregačního kanálu, kterou MongoDB používá k vrácení odpovídajících položek z jiné kolekce. Na rozdíl od populate() to se ve skutečnosti provádí v jediném požadavku na server s jedinou odpovědí. Tím se vyhnete režii sítě, je obecně rychlejší a jako "skutečné spojení" vám umožní dělat věci, které populate() nemůže udělat.

Použijte místo toho $lookup

Velmi rychlé verze toho, co zde chybí, je, že namísto pokusu o populate() v .then() po vrácení výsledku místo toho přidáte $lookup do potrubí:

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

Všimněte si, že zde existuje omezení v tom, že výstup $lookup je vždy pole. Nezáleží na tom, zda existuje pouze jedna související položka nebo mnoho, které mají být načteny jako výstup. Fáze kanálu bude hledat hodnotu "localField" z aktuálního prezentovaného dokumentu a použijte jej ke spárování hodnot v "foreignField" specifikováno. V tomto případě je to _id z agregace $group cílit na _id zahraniční sbírky.

Protože výstup je vždy pole jak bylo zmíněno, nejúčinnějším způsobem, jak s tím v této instanci pracovat, by bylo jednoduše přidat $unwind fázi přímo navazující na $lookup . To vše vrátí nový dokument pro každou položku vrácenou v cílovém poli a v tomto případě očekáváte, že to bude jeden. V případě, že _id neodpovídá v zahraniční kolekci, výsledky bez shody budou odstraněny.

Malá poznámka, toto je ve skutečnosti optimalizovaný vzor, ​​jak je popsáno v $lookup + $unwind Coalescence v rámci základní dokumentace. Zde se stane zvláštní věc, když $unwind instrukce je ve skutečnosti sloučena do $lookup provozovat efektivním způsobem. Zde si o tom můžete přečíst více.

Pomocí naplnění

Z výše uvedeného obsahu byste měli být schopni v podstatě pochopit, proč populate() tady je špatná věc. Kromě základního faktu, že výstup již neobsahuje Warranty modelové objekty, tento model ve skutečnosti zná pouze cizí položky popsané v _accountId vlastnost, která stejně ve výstupu neexistuje.

Nyní můžete skutečně definovat model, který lze použít k explicitnímu přetypování výstupních objektů do definovaného výstupního typu. Krátká ukázka jednoho by zahrnovala přidání kódu do vaší aplikace, například:

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

Tento nový Output model lze poté použít k „osazení“ výsledných prostých objektů JavaScriptu do dokumentů Mongoose, takže metody jako Model.populate() lze ve skutečnosti nazvat:

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

Od Output má definované schéma, které si je vědomo "odkazu" na _id pole jeho dokumentů Model.populate() ví, co musí udělat, a vrátí položky.

Dejte si však pozor, protože to ve skutečnosti generuje další dotaz. tj.:

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

Kde první řádek je souhrnný výstup a poté znovu kontaktujete server, abyste vrátili související Account položky modelu.

Shrnutí

Takže toto jsou vaše možnosti, ale mělo by být docela jasné, že moderní přístup k tomu je místo toho použít $lookup a získejte skutečné „připojení“ což není to, co populate() skutečně dělá.

Součástí je seznam jako úplná ukázka toho, jak každý z těchto přístupů skutečně funguje v praxi. Nějaká umělecká licence je převzato zde, takže zastoupené modely nemusí být přesně stejné jako to, co máte, ale je toho dost na to, aby bylo možné demonstrovat základní pojmy reprodukovatelným způsobem:

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

})()

A úplný výstup:

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. Změňte existující objekt v poli, ale přesto zachovejte jedinečnost klíče

  2. Chyba při povolování šifrování dat pomocí místního klíče MONGODB

  3. Hromadná aktualizace/náhrada Mongodb C# v podsbírce

  4. passport.js – ověřuje uživatele z MongoDB pomocí passport-local