MongoDB
 sql >> Teknologi Basis Data >  >> NoSQL >> MongoDB

Populasi Luwak setelah Agregat

Jadi Anda sebenarnya kehilangan beberapa konsep di sini ketika Anda meminta untuk "mengisi" pada hasil agregasi. Biasanya ini bukan apa yang sebenarnya Anda lakukan, tetapi untuk menjelaskan poin-poinnya:

  1. Output dari aggregate() tidak seperti Model.find() atau tindakan serupa karena tujuannya di sini adalah untuk "membentuk kembali hasil". Ini pada dasarnya berarti bahwa model yang Anda gunakan sebagai sumber agregasi tidak lagi dianggap sebagai model pada output. Ini bahkan benar jika Anda masih mempertahankan struktur dokumen yang sama persis pada keluaran, tetapi dalam kasus Anda, keluarannya jelas berbeda dengan dokumen sumber.

    Bagaimanapun itu bukan lagi contoh Warranty model yang Anda sumber, tetapi hanya objek biasa. Kita bisa mengatasinya saat kita membahasnya nanti.

  2. Mungkin poin utama di sini adalah populate() agak "topi tua" omong-omong. Ini benar-benar hanya fungsi kenyamanan yang ditambahkan ke Mongoose pada hari-hari awal implementasi. Yang benar-benar dilakukannya hanyalah menjalankan "permintaan lain" pada terkait data dalam koleksi terpisah, lalu menggabungkan hasilnya di memori ke output koleksi asli.

    Untuk banyak alasan, itu tidak terlalu efisien atau bahkan diinginkan dalam banyak kasus. Dan bertentangan dengan kesalahpahaman populer, ini TIDAK sebenarnya "bergabung".

    Untuk "bergabung" yang sebenarnya, Anda sebenarnya menggunakan $lookup tahap pipa agregasi, yang digunakan MongoDB untuk mengembalikan item yang cocok dari koleksi lain. Tidak seperti populate() ini sebenarnya dilakukan dalam satu permintaan ke server dengan satu respons. Ini menghindari overhead jaringan, umumnya lebih cepat dan sebagai "gabungan nyata" memungkinkan Anda melakukan hal-hal yang populate() tidak bisa.

Gunakan $lookup sebagai gantinya

Sangat cepat versi dari apa yang hilang di sini adalah alih-alih mencoba populate() di .then() setelah hasilnya dikembalikan, yang Anda lakukan adalah menambahkan $lookup ke saluran:

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

Perhatikan bahwa ada batasan di sini bahwa output dari $lookup adalah selalu sebuah array. Tidak masalah jika hanya ada satu item terkait atau banyak yang akan diambil sebagai output. Tahap pipeline akan mencari nilai "localField" dari dokumen saat ini yang disajikan dan gunakan itu untuk mencocokkan nilai di "foreignField" ditentukan. Dalam hal ini adalah _id dari agregasi $group targetkan ke _id koleksi asing.

Karena outputnya selalu berupa array seperti yang disebutkan, cara paling efisien untuk bekerja dengan ini untuk contoh ini adalah dengan menambahkan $unwind panggung langsung mengikuti $lookup . Semua ini akan mengembalikan dokumen baru untuk setiap item yang dikembalikan dalam array target, dan dalam hal ini Anda mengharapkannya menjadi satu. Dalam hal _id tidak cocok dalam koleksi asing, hasil tanpa kecocokan akan dihapus.

Sebagai catatan kecil, ini sebenarnya adalah pola yang dioptimalkan seperti yang dijelaskan dalam $lookup + $unwind Coalescence dalam dokumentasi inti. Hal khusus terjadi di sini di mana $unwind instruksi sebenarnya digabungkan ke dalam $lookup operasi dengan cara yang efisien. Anda dapat membaca lebih lanjut tentang itu di sana.

Menggunakan populate

Dari konten di atas, pada dasarnya Anda harus dapat memahami mengapa populate() di sini adalah hal yang salah untuk dilakukan. Selain fakta dasar bahwa output tidak lagi terdiri dari Warranty objek model, model itu benar-benar hanya tahu tentang item asing yang dijelaskan di _accountId properti yang tidak ada di output.

Sekarang Anda bisa sebenarnya mendefinisikan model yang dapat digunakan untuk secara eksplisit melemparkan objek output ke dalam tipe output yang ditentukan. Demonstrasi singkat akan melibatkan penambahan kode ke aplikasi Anda untuk ini seperti:

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

Output baru ini model kemudian dapat digunakan untuk "melempar" objek JavaScript biasa yang dihasilkan ke dalam Dokumen Mongoose sehingga metode seperti Model.populate() sebenarnya bisa dipanggil:

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

Sejak Output memiliki skema yang ditentukan yang mengetahui "referensi" pada _id bidang dokumennya Model.populate() mengetahui apa yang perlu dilakukan dan mengembalikan item.

Namun berhati-hatilah karena ini sebenarnya menghasilkan kueri lain. yaitu:

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

Di mana baris pertama adalah output agregat, dan kemudian Anda menghubungi server lagi untuk mengembalikan Account terkait entri model.

Ringkasan

Jadi itu adalah pilihan Anda, tetapi harus cukup jelas bahwa pendekatan modern untuk ini adalah menggunakan $lookup dan dapatkan "gabung" yang sebenarnya yang bukan populate() sebenarnya sedang dilakukan.

Termasuk adalah daftar sebagai demonstrasi penuh tentang bagaimana masing-masing pendekatan ini benar-benar bekerja dalam praktik. Beberapa lisensi artistik diambil di sini, jadi model yang diwakili mungkin tidak persis sama dengan yang Anda miliki, tetapi cukup untuk mendemonstrasikan konsep dasar dengan cara yang dapat direproduksi:

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

})()

Dan hasil lengkapnya:

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. Mengapa PyMongo 3 memberikan ServerSelectionTimeoutError?

  2. Cara Mengotomatiskan dan Mengelola MongoDB Dengan ClusterControl

  3. Bagaimana cara mengambil data dari koleksi MongoDB di C # menggunakan Ekspresi Reguler?

  4. Kelola Multiprocessing Python dengan MongoDB

  5. Mengelola Beberapa Teknologi Basis Data dengan ClusterControl