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

Запитване след попълване в Mongoose

С модерен MongoDB, по-висок от 3.2, можете да използвате $lookup като алтернатива на .populate() в повечето случаи. Това също има предимството, че всъщност се прави присъединяването "на сървъра", за разлика от това, което .populate() прави, което всъщност е "множество заявки" за "емулиране" присъединяване.

Така че .populate() ене наистина "присъединяване" в смисъл как го прави релационна база данни. $lookup операторът, от друга страна, всъщност върши работата на сървъра и е повече или по-малко аналогичен на „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 тук всъщност се оценява на "низ", който е действителното име на колекцията MongoDB, както е присвоено на модела. Тъй като mongoose „множава“ имената на колекции по подразбиране и $lookup се нуждае от действителното име на колекцията MongoDB като аргумент (тъй като това е операция на сървъра), тогава това е удобен трик за използване в кода на mongoose, за разлика от директното „твърдо кодиране“ на името на колекцията.

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

Това всъщност води до обединяване на трите етапа на тръбопровода в един:

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

Това е много оптимално, тъй като действителната операция "филтрира колекцията, за да се присъедини първо", след това връща резултатите и "развива" масива. Използват се и двата метода, така че резултатите да не нарушават ограничението на BSON от 16MB, което е ограничение, което клиентът няма.

Единственият проблем е, че изглежда "противоинтуитивно" в някои отношения, особено когато искате резултатите в масив, но това е, което $group е за тук, тъй като се реконструира до оригиналния формуляр на документа.

Също така е жалко, че в момента просто не можем да напишем $lookup в същия евентуален синтаксис, който използва сървърът. ИМХО, това е пропуск, който трябва да се коригира. Но засега простото използване на последователността ще работи и е най-жизнеспособната опция с най-добра производителност и мащабируемост.

Допълнение - MongoDB 3.6 и по-нова версия

Въпреки че показаният тук модел е доста оптимизиран поради това как другите етапи се включват в $lookup , той има един неуспешен в това "LEFT JOIN", което обикновено е присъщо и на двете $lookup и действията на populate() се отрича от "оптималното" използване на $unwind тук, което не запазва празни масиви. Можете да добавите preserveNullAndEmptyArrays опция, но това отрича „оптимизирано“ последователността, описана по-горе, и по същество оставя и трите етапа непокътнати, които нормално биха били комбинирани в оптимизацията.

MongoDB 3.6 се разширява с „по-изразителен“ форма на $lookup позволяващ израз "под-тръбопровод". Което не само отговаря на целта за запазване на "LEFT JOIN", но все пак позволява оптимална заявка за намаляване на върнатите резултати и с много опростен синтаксис:

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

$expr използвано, за да се съпостави декларираната "локална" стойност с "чуждата" стойност, всъщност е това, което MongoDB прави "вътрешно" сега с оригиналния $lookup синтаксис. Чрез изразяване в тази форма можем да приспособим първоначалния $match израз в рамките на самите "подтръбопровод".

Всъщност, като истински „тръбопровод за агрегиране“ вие можете да правите почти всичко, което можете да правите с конвейер за агрегиране в рамките на този израз на „подтръбопровод“, включително „влагане“ на нивата на $lookup към други свързани колекции.

По-нататъшното използване е малко извън обхвата на това, което задава въпросът тук, но във връзка с дори „вложена популация“ тогава новият модел на използване на $lookup позволява това да бъде почти същото и „много“ по-мощен при пълното му използване.

Работен пример

По-долу е даден пример за използване на статичен метод върху модела. След като този статичен метод бъде внедрен, извикването просто става:

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

Или подобряването, за да бъдете малко по-модерни, дори става:

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

Правейки го много подобно на .populate() в структура, но всъщност вместо това прави присъединяването на сървъра. За пълнота, използването тук прехвърля върнатите данни обратно към екземпляри на документ mongoose в според родителския и дъщерния случай.

Той е доста тривиален и лесен за адаптиране или просто за използване, както е в повечето често срещани случаи.

N.B Използването на async тук е само за краткост на изпълнение на приложения пример. Действителната реализация е свободна от тази зависимост.

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

Или малко по-модерно за Node 8.x и по-нови с async/await и без допълнителни зависимости:

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

И от MongoDB 3.6 и нагоре, дори без $unwind и $group сграда:

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. Как да върна резултатите от Mongoose от метода за намиране?

  2. Мангуста и уникално поле

  3. как да освободя кеширането, което се използва от Mongodb?

  4. 6 полезни инструмента за наблюдение на производителността на MongoDB

  5. MongoDB Връзка едно към много