Запит після заселення в Мангусті


83

Я досить новачок у Mongoose та MongoDB загалом, тому мені важко зрозуміти, чи можливо щось подібне:

Item = new Schema({
    id: Schema.ObjectId,
    dateCreated: { type: Date, default: Date.now },
    title: { type: String, default: 'No Title' },
    description: { type: String, default: 'No Description' },
    tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});

ItemTag = new Schema({
    id: Schema.ObjectId,
    tagId: { type: Schema.ObjectId, ref: 'Tag' },
    tagName: { type: String }
});



var query = Models.Item.find({});

query
    .desc('dateCreated')
    .populate('tags')
    .where('tags.tagName').in(['funny', 'politics'])
    .run(function(err, docs){
       // docs is always empty
    });

Чи є кращий спосіб це зробити?

Редагувати

Вибачення за будь-яку плутанину. Я намагаюся отримати всі предмети, які містять або смішний тег, або тег політики.

Редагувати

Документ без пропозиції where:

[{ 
    _id: 4fe90264e5caa33f04000012,
    dislikes: 0,
    likes: 0,
    source: '/uploads/loldog.jpg',
    comments: [],
    tags: [{
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'movies',
        tagId: 4fe64219007e20e644000007,
        _id: 4fe90270e5caa33f04000015,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    },
    { 
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'funny',
        tagId: 4fe64219007e20e644000002,
        _id: 4fe90270e5caa33f04000017,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    }],
    viewCount: 0,
    rating: 0,
    type: 'image',
    description: null,
    title: 'dogggg',
    dateCreated: Tue, 26 Jun 2012 00:29:24 GMT 
 }, ... ]

За допомогою речення where я отримую порожній масив.

Відповіді:


61

З сучасним 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"
  }
)

Примітка . .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 у 16 ​​МБ, що є обмеженням, якого немає у клієнта.

Єдина проблема полягає в тому, що це здається "протиінтуїтивним" певним чином, особливо коли ви хочете отримати результати в масиві, але для цього він і потрібен $group, оскільки він перетворюється на вихідну форму документа.

Також прикро, що в цей час ми просто не можемо писати $lookupтим самим можливим синтаксисом, який використовує сервер. ІМХО, це недогляд, який слід виправити. Але наразі просто використання послідовності буде працювати, і це найбільш життєздатний варіант з найкращою продуктивністю та масштабованістю.

Додаток - MongoDB 3.6 і вище

Незважаючи на те, що показаний тут шаблон досить оптимізований завдяки тому, як інші етапи вкочуються в $lookup, він має один недолік, оскільки "ЛІВЕ ПРИЄДНАННЯ", яке зазвичай властиве обом, $lookupі дії "" populate()заперечуються "оптимальним" використанням $unwindтут не зберігаються порожні масиви. Ви можете додати preserveNullAndEmptyArraysопцію, але це заперечує описану вище «оптимізовану» послідовність і по суті залишає цілими всі три етапи, які зазвичай поєднуються в оптимізації.

MongoDB 3.6 розширюється за допомогою "більш виразної" форми, $lookupдозволяючи вираз "під конвеєр". Що не тільки відповідає меті збереження "ЛІВОГО ПРИЄДНАННЯ", але все ще дозволяє оптимальному запиту зменшити отримані результати та з набагато спрощеним синтаксисом:

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()структуру, але він фактично робить об'єднання на сервері. Для повноти використання тут повертає дані, що повертаються, до екземплярів мангустських документів, відповідно до батьківського та дочірнього випадків.

Це досить тривіально і легко адаптується або просто використовується, як це є в більшості випадків.

NB : Використання асинхронізації тут призначене лише для стислості запуску прикладеного прикладу. Фактична реалізація вільна від цієї залежності.

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

})()

3
Я більше не використовую Mongo / Mongoose, але я прийняв вашу відповідь, оскільки це популярне запитання, і, схоже, це було корисно для інших. Радий бачити, що ця проблема зараз має більш масштабоване рішення. Дякуємо за надану оновлену відповідь.
jschr

40

те, про що ви просите, не підтримується безпосередньо, але його можна досягти, додавши ще один крок фільтра після повернення запиту.

по-перше, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )це точно те, що вам потрібно зробити, щоб відфільтрувати документи тегів. потім, після повернення запиту, вам потрібно буде вручну відфільтрувати документи, які не мають жодних tagsдокументів, що відповідають критеріям заповнення. щось на зразок:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});

1
Гей, Аароне, дякую за відповідь. Можливо, я помиляюся, але чи не $ in on populate () заповнює лише відповідні теги? Тож будь-які додаткові теги на товарі будуть відфільтровані. Здається, мені доведеться заповнити всі елементи, а другий крок фільтру зменшити його на основі імені тегу.
jschr

@aaronheckmann Я реалізував запропоноване вами рішення, ви маєте рацію зробити фільтр після .exec, оскільки, хоча запит на заповнення заповнює лише необхідні об'єкти, але все ще повертає весь набір даних. Чи вважаєте ви, що в новішій версії Mongoose є можливість повернути лише заповнений набір даних, тому нам не потрібно переходити до іншої фільтрації?
Aqib Mumtaz

Мені також цікаво дізнатись про продуктивність. Якщо запит повертає весь набір даних в кінці, тоді немає мети переходити до фільтрації сукупності? Що ти сказав? Я адаптую запит популяції для оптимізації продуктивності, але таким чином продуктивність не покращується для великих наборів даних?
Aqib Mumtaz

mongoosejs.com/docs/api.html#query_Query-populate має всі подробиці, якщо хтось ще зацікавлений
самазі

як збігаються в різних полях при заповненні?
nicogaldo

20

Спробуйте замінити

.populate('tags').where('tags.tagName').in(['funny', 'politics']) 

від

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )

1
Дякую за відповідь. Я вважаю, що це робить лише заповнення кожного елемента забавним чи політичним, що не зменшить батьківський список. Що б я насправді хотів, це лише предмети, що містять забавне або політичне.
jschr

Чи можете ви показати, як виглядає ваш документ? Coz a 'where' усередині масиву тегів здається мені дійсною операцією .. Ми просто помиляємося в синтаксисі .. Ви намагалися повністю видалити це речення 'where' і перевірили, чи повертається що-небудь? В якості альтернативи, просто щоб перевірити, чи написання 'tags.tagName' є синтаксично нормальним, ви можете на деякий час забути річ ref і спробувати свій запит із вбудованим масивом всередині документа 'Item'.
Aafreen Sheikh

Редагував мою оригінальну публікацію з документом. Я зміг протестувати його з моделлю як вбудований масив всередині елемента з успіхом, але, на жаль, я вимагаю, щоб це був DBRef, оскільки ItemTag часто оновлюється. Ще раз спасибі за допомогу.
jschr

15

Оновлення: Будь ласка, погляньте на коментарі - ця відповідь не відповідає запитанню, але, можливо, вона відповідає на інші запитання користувачів, які зіткнулися (я думаю, що через голоси "за"), тому я не буду видаляти цю "відповідь":

По-перше: я знаю, що це питання насправді застаріле, але я шукав саме цю проблему, і ця публікація SO була записом Google №1. Тож я реалізував docs.filterверсію (прийнята відповідь), але, як я прочитав у документах мангуста v4.6.0, тепер ми можемо просто використовувати:

Item.find({}).populate({
    path: 'tags',
    match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
  console.log(items.tags) 
  // contains only tags where tagName is 'funny' or 'politics'
})

Сподіваюся, це допоможе майбутнім користувачам пошукових машин.


3
Але це напевно лише відфільтрує масив items.tags? Предмети повертатимуться незалежно від tagName ...
OllyBarca

1
Це правильно, @OllyBarca. Згідно з документами, збіг впливає лише на запит про сукупність.
andreimarinescu

1
Я думаю, це не відповідає на запитання
Z.Alpha

1
@Fabian, що не є помилкою. Фільтрується лише запит про сукупність (у цьому випадку fans). Фактичний повернутий документ (який є Story, містить fansяк властивість) не впливає або не фільтрується.
EnKrypt

2
Таким чином, ця відповідь не є правильною з причин, зазначених у коментарях. Той, хто дивиться на це в майбутньому, повинен бути обережним.
EnKrypt

3

Після того, як нещодавно у мене була така сама проблема, я придумав таке рішення:

Спочатку знайдіть усі ItemTag, де tagName є або «забавним», або «політичним», і поверніть масив ItemTag _ids.

Потім знайдіть елементи, які містять усі ідентифікатори ItemTag _ у масиві тегів

ItemTag
  .find({ tagName : { $in : ['funny','politics'] } })
  .lean()
  .distinct('_id')
  .exec((err, itemTagIds) => {
     if (err) { console.error(err); }
     Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
        console.log(items); // Items filtered by tagName
     });
  });

Як я це зробив const tagsIds = await this.tagModel .find ({name: {$ in: tags}}) .lean () .distinct ('_ id'); повернути this.adviceModel.find ({теги: {$ all: tagsIds}});
Dragos Lupei

1

Відповідь @aaronheckmann спрацювала для мене, але мені довелося замінити return doc.tags.length;на, return doc.tags != null;оскільки це поле містить null, якщо воно не відповідає умовам, написаним всередині заповнення. Отже, остаточний код:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.