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

Menanyakan setelah diisi di Mongoose

Dengan MongoDB modern yang lebih besar dari 3,2 Anda dapat menggunakan $lookup sebagai pengganti .populate() umumnya. Ini juga memiliki keuntungan untuk benar-benar melakukan penggabungan "di server" sebagai lawan dari .populate() apakah yang sebenarnya "beberapa kueri" untuk "ditiru" bergabung.

Jadi .populate() adalah tidak benar-benar "bergabung" dalam arti bagaimana database relasional melakukannya. $lookup operator di sisi lain, benar-benar bekerja di server, dan kurang lebih analog dengan "LEFT JOIN" :

Item.aggregate(
  [
    { "$lookup": {
      "from": ItemTags.collection.name,
      "localField": "tags",
      "foreignField": "_id",
      "as": "tags"
    }},
    { "$unwind": "$tags" },
    { "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
    { "$group": {
      "_id": "$_id",
      "dateCreated": { "$first": "$dateCreated" },
      "title": { "$first": "$title" },
      "description": { "$first": "$description" },
      "tags": { "$push": "$tags" }
    }}
  ],
  function(err, result) {
    // "tags" is now filtered by condition and "joined"
  }
)

N.B. .collection.name di sini sebenarnya mengevaluasi ke "string" yang merupakan nama sebenarnya dari koleksi MongoDB seperti yang ditetapkan untuk model. Sejak luwak "memperbanyak" nama koleksi secara default dan $lookup membutuhkan nama koleksi MongoDB yang sebenarnya sebagai argumen (karena ini adalah operasi server), maka ini adalah trik praktis untuk digunakan dalam kode luwak, sebagai lawan dari "pengkodean keras" nama koleksi secara langsung.

Sementara kita juga bisa menggunakan $filter pada array untuk menghapus item yang tidak diinginkan, ini sebenarnya adalah bentuk yang paling efisien karena Agregasi Pipeline Optimization untuk kondisi khusus sebagai $lookup diikuti oleh $unwind dan $match kondisi.

Ini sebenarnya menghasilkan tiga tahap saluran yang digabung menjadi satu:

   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

Ini sangat optimal karena operasi sebenarnya "memfilter koleksi untuk bergabung terlebih dahulu", lalu mengembalikan hasil dan "melepas" array. Kedua metode digunakan agar hasilnya tidak melanggar batas BSON sebesar 16 MB, yang merupakan batasan yang tidak dimiliki klien.

Satu-satunya masalah adalah tampaknya "kontra-intuitif" dalam beberapa hal, terutama ketika Anda menginginkan hasil dalam array, tetapi itulah yang $group adalah untuk di sini, karena direkonstruksi ke bentuk dokumen asli.

Sayangnya, saat ini kami tidak dapat benar-benar menulis $lookup dalam sintaks akhirnya yang sama yang digunakan server. IMHO, ini adalah kekeliruan yang harus diperbaiki. Namun untuk saat ini, hanya menggunakan urutan akan berhasil dan merupakan opsi yang paling memungkinkan dengan kinerja dan skalabilitas terbaik.

Addendum - MongoDB 3.6 dan lebih tinggi

Meskipun pola yang ditampilkan di sini cukup dioptimalkan karena bagaimana tahapan lainnya dimasukkan ke dalam $lookup , ia memiliki satu kegagalan dalam "LEFT JOIN" yang biasanya melekat pada keduanya $lookup dan tindakan populate() dinegasikan oleh "optimal" penggunaan $unwind di sini yang tidak mempertahankan array kosong. Anda dapat menambahkan preserveNullAndEmptyArrays opsi, tetapi ini meniadakan "dioptimalkan" urutan yang dijelaskan di atas dan pada dasarnya membiarkan ketiga tahap tetap utuh yang biasanya digabungkan dalam pengoptimalan.

MongoDB 3.6 berkembang dengan "lebih ekspresif" bentuk $lookup memungkinkan ekspresi "sub-pipa". Yang tidak hanya memenuhi tujuan mempertahankan "LEFT JOIN" tetapi masih memungkinkan kueri yang optimal untuk mengurangi hasil yang dikembalikan dan dengan sintaks yang jauh lebih sederhana:

Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

$expr digunakan untuk mencocokkan nilai "lokal" yang dideklarasikan dengan nilai "asing" sebenarnya adalah apa yang MongoDB lakukan "secara internal" sekarang dengan $lookup asli sintaksis. Dengan mengekspresikan dalam formulir ini kita dapat menyesuaikan $match awal ekspresi dalam "sub-pipa" itu sendiri.

Faktanya, sebagai "pipa agregasi" sejati, Anda dapat melakukan apa saja yang dapat Anda lakukan dengan pipa agregasi dalam ekspresi "sub-pipa" ini, termasuk "menyarangkan" level $lookup ke koleksi terkait lainnya.

Penggunaan lebih lanjut sedikit di luar cakupan pertanyaan di sini, tetapi dalam kaitannya dengan bahkan "populasi bersarang" maka pola penggunaan baru $lookup memungkinkan ini menjadi hampir sama, dan "lot" lebih kuat dalam penggunaan penuhnya.

Contoh Kerja

Berikut ini adalah contoh penggunaan metode statis pada model. Setelah metode statis diimplementasikan, panggilan menjadi:

  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

Atau ditingkatkan menjadi sedikit lebih modern bahkan menjadi:

  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

Membuatnya sangat mirip dengan .populate() dalam struktur, tetapi sebenarnya melakukan penggabungan di server sebagai gantinya. Untuk kelengkapan, penggunaan di sini mengembalikan data yang dikembalikan ke instance dokumen luwak sesuai dengan kasus induk dan anak.

Ini cukup sepele dan mudah untuk diadaptasi atau digunakan sebagaimana adanya untuk sebagian besar kasus umum.

N.B Penggunaan async di sini hanya untuk singkatnya menjalankan contoh terlampir. Implementasi sebenarnya bebas dari ketergantungan ini.

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt,callback) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  this.aggregate(pipeline,(err,result) => {
    if (err) callback(err);
    result = result.map(m => {
      m[opt.path] = m[opt.path].map(r => rel(r));
      return this(m);
    });
    callback(err,result);
  });
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

function log(body) {
  console.log(JSON.stringify(body, undefined, 2))
}
async.series(
  [
    // Clean data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create tags and items
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
              callback),

          (tags, callback) =>
            Item.create({ "title": "Something","description": "An item",
              "tags": tags },callback)
        ],
        callback
      ),

    // Query with our static
    (callback) =>
      Item.lookup(
        {
          path: 'tags',
          query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
        },
        callback
      )
  ],
  (err,results) => {
    if (err) throw err;
    let result = results.pop();
    log(result);
    mongoose.disconnect();
  }
)

Atau sedikit lebih modern untuk Node 8.x ke atas dengan async/await dan tidak ada dependensi tambahan:

const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m => 
    this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
  ));
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {
  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.create(
      ["movies", "funny"].map(tagName =>({ tagName }))
    );
    const item = await Item.create({ 
      "title": "Something",
      "description": "An item",
      tags 
    });

    // Query with our static
    const result = (await Item.lookup({
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);

    mongoose.disconnect();

  } catch (e) {
    console.error(e);
  } finally {
    process.exit()
  }
})()

Dan dari MongoDB 3.6 ke atas, bahkan tanpa $unwind dan $group bangunan:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });

itemSchema.statics.lookup = function({ path, query }) {
  let rel =
    mongoose.model(this.schema.path(path).caster.options.ref);

  // MongoDB 3.6 and up $lookup with sub-pipeline
  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": path,
      "let": { [path]: `$${path}` },
      "pipeline": [
        { "$match": {
          ...query,
          "$expr": { "$in": [ "$_id", `$$${path}` ] }
        }}
      ]
    }}
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m =>
    this({ ...m, [path]: m[path].map(r => rel(r)) })
  ));
};

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.insertMany(
      ["movies", "funny"].map(tagName => ({ tagName }))
    );

    const item = await Item.create({
      "title": "Something",
      "description": "An item",
      tags
    });

    // Query with our static
    let result = (await Item.lookup({
      path: 'tags',
      query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);


    await mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Deserialisasi tipe polimorfik dengan MongoDB C# Driver

  2. MongoDB C# Kueri untuk 'Suka' pada string

  3. Bagaimana saya bisa membuat daftar semua koleksi di shell MongoDB?

  4. Bagaimana cara terhubung dengan mongodb menggunakan sailsjs v0.10?

  5. Proyeksikan item pertama dalam larik ke bidang baru (agregasi MongoDB)