MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Populacja mangusty po zagregowaniu

Tak więc brakuje tu niektórych pojęć, gdy prosisz o „wypełnienie” wyniku agregacji. Zazwyczaj nie jest to to, co faktycznie robisz, ale wyjaśnienie punktów:

  1. Wyjście aggregate() różni się od Model.find() lub podobne działanie, ponieważ celem jest „przekształcenie wyników”. Zasadniczo oznacza to, że model używany jako źródło agregacji nie jest już uważany za model na wyjściu. Dzieje się tak nawet wtedy, gdy nadal zachowujesz dokładnie tę samą strukturę dokumentu na wyjściu, ale w Twoim przypadku wynik i tak wyraźnie różni się od dokumentu źródłowego.

    W każdym razie nie jest to już instancja Gwarancji model, z którego czerpiesz, ale tylko zwykły obiekt. Możemy to obejść, gdy porozmawiamy później.

  2. Prawdopodobnie głównym punktem tutaj jest to, że populate() jest trochę "stary kapelusz" w każdym razie. Jest to tak naprawdę tylko wygodna funkcja dodana do Mongoose w bardzo wczesnych dniach wdrażania. Wszystko, co tak naprawdę robi, to wykonanie „kolejnego zapytania” na powiązanym dane w oddzielnej kolekcji, a następnie scala wyniki w pamięci z oryginalnym wyjściem kolekcji.

    Z wielu powodów nie jest to zbyt wydajne, a nawet pożądane w większości przypadków. I w przeciwieństwie do popularnego błędnego przekonania, NIE właściwie „dołączenie”.

    Do prawdziwego „dołączenia” używasz $lookup etap potoku agregacji, którego MongoDB używa do zwracania pasujących elementów z innej kolekcji. W przeciwieństwie do populate() w rzeczywistości odbywa się to w pojedynczym żądaniu do serwera z pojedynczą odpowiedzią. Pozwala to uniknąć narzutów sieciowych, jest generalnie szybsze i jako „prawdziwe dołączenie” pozwala robić rzeczy, które wypełniają() nie mogę tego zrobić.

Zamiast tego użyj wyszukiwania $

Bardzo szybkie brakującą wersją jest to, że zamiast próbować wypełniać() w .then() po zwróceniu wyniku zamiast tego dodajesz $oglądaj do rurociągu:

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

Zauważ, że istnieje tutaj ograniczenie, ponieważ wyjście $ wyszukiwanie jest zawsze tablica. Nie ma znaczenia, czy istnieje tylko jeden powiązany element, czy wiele do pobrania jako dane wyjściowe. Etap potoku będzie szukał wartości "localField" z bieżącego prezentowanego dokumentu i użyj go do dopasowania wartości w "foreignField" określony. W tym przypadku jest to _id z agregacji $group cel na _id kolekcji zagranicznej.

Ponieważ wyjście jest zawsze tablicą jak wspomniano, najskuteczniejszym sposobem pracy z tym w tym przypadku byłoby po prostu dodanie $unwind etap bezpośrednio po $lookup . Wszystko to spowoduje zwrócenie nowego dokumentu dla każdego elementu zwróconego w tablicy docelowej, aw tym przypadku oczekujesz, że będzie to jeden. W przypadku, gdy _id nie zostanie dopasowany w kolekcji zagranicznej, wyniki bez dopasowań zostaną usunięte.

Na marginesie, jest to w rzeczywistości zoptymalizowany wzorzec opisany w $ lookup + $unwind Koalescencja w ramach podstawowej dokumentacji. Wyjątkowa rzecz dzieje się tutaj, gdy $unwind instrukcja jest w rzeczywistości połączona z $lookup w efektywny sposób. Więcej na ten temat możesz przeczytać tam.

Korzystanie z wypełniania

Z powyższej treści powinieneś być w stanie zrozumieć, dlaczego populate() tutaj jest zła rzecz do zrobienia. Poza podstawowym faktem, że dane wyjściowe nie są już objęte Gwarancją obiekty modelu, ten model tak naprawdę wie tylko o obcych elementach opisanych w _accountId właściwość, która i tak nie istnieje w danych wyjściowych.

Teraz możesz faktycznie zdefiniować model, którego można użyć w celu jawnego rzutowania obiektów wyjściowych na określony typ wyjściowy. Krótka demonstracja jednego z nich wymagałaby dodania kodu do aplikacji w następujący sposó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');

To nowe Wyjście model może być następnie użyty w celu "rzucenia" wynikowych zwykłych obiektów JavaScript do Mongoose Documents, tak aby metody takie jak Model.populate() można nazwać:

// 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 Wyjście ma zdefiniowany schemat, który jest świadomy „odniesienia” w _id pole jego dokumentów Model.populate() jest świadomy tego, co musi zrobić i zwraca przedmioty.

Uważaj jednak, ponieważ to faktycznie generuje kolejne zapytanie. czyli:

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

Gdzie pierwsza linia to zagregowane dane wyjściowe, a następnie ponownie kontaktujesz się z serwerem w celu zwrócenia powiązanego Konta wpisy modeli.

Podsumowanie

Więc to są twoje opcje, ale powinno być całkiem jasne, że nowoczesne podejście do tego polega na użyciu $lookup i uzyskaj prawdziwe „dołączenie” co nie jest tym, co wypełnij() faktycznie robi.

Dołączona jest lista jako pełna demonstracja tego, jak każde z tych podejść faktycznie działa w praktyce. Niektóre licencje artystyczne jest tutaj wzięty, więc przedstawione modele mogą nie być dokładnie tak samo, jak masz, ale wystarczy, aby zademonstrować podstawowe pojęcia w powtarzalny sposób:

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

})()

I pełne wyjście:

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. Unikalne ograniczenia MongoDb w zakresie dat

  2. MongoDB oblicza wynik z istniejących pól i umieszcza go w nowym polu w tej samej kolekcji

  3. Jak osiągnąć wycofanie w transakcjach w mongo?

  4. Wybierz dokument mający określoną parę klucz-wartość, ale nie mający innej pary klucz-wartość

  5. Tablica Mongodb $push i $pull