Запрос после заполнения в Mongoose

Я довольно новичок в 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 я получаю пустой массив.

При использовании современного MongoDB, превышающего 3,2, вы можете использовать $lookup в качестве альтернативы .populate() в большинстве случаев. Это также имеет преимущество, заключающееся в том, что на самом деле выполняется соединение «на сервере», а не то, что .populate() , что на самом деле «несколько запросов» для «подражания» соединения.

Таким образом .populate() самом деле не является «объединением» в смысле того, как это делает реляционная firebase database. Оператор $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" } ) 

NB Здесь .collection.name фактически оценивает «строку», которая является фактическим именем коллекции MongoDB, присвоенной модели. Поскольку mongoose «плюрализует» имена коллекций по умолчанию, а $lookup требует фактического имени коллекции MongoDB в качестве аргумента (поскольку это операция с сервером), то это удобный трюк для использования в коде Mongoose, в отличие от «жесткого кодирования» коллекции имя напрямую.

Хотя мы могли также использовать $filter на массивах для удаления нежелательных элементов, это фактически самая эффективная форма из-за оптимизации Aggregation Pipeline Optimization для специального условия как $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 , у нее есть одна неудача в том, что «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 в другие связанные коллекции.

Дальнейшее использование немного выходит за frameworks того, что здесь задается здесь, но в отношении даже «вложенной совокупности» тогда новая модель использования $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 в соответствии с родительским и дочерним случаями.

Это довольно тривиально и легко адаптируется или просто используется как для большинства распространенных случаев.

NB Использование 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() } })() 

то, что вы просите, напрямую не поддерживается, но может быть достигнуто путем добавления другого шага фильтра после возвращения запроса.

сначала, .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 }); 

Попробуйте заменить

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

от

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

Обновление. Пожалуйста, взгляните на комментарии – этот ответ не соответствует вашему вопросу, но, возможно, он отвечает на другие вопросы пользователей, которые натолкнулись (я думаю, что из-за upvotes), поэтому я не буду удалять этот «ответ»:

Во-первых: Я знаю, что этот вопрос действительно устарел, но я искал именно эту проблему, и эта запись SO была в Google # 1. Поэтому я внедрил версию docs.filter (принятый ответ), но, как я читал в документах mongoose 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' }) 

Надеюсь, это поможет будущим пользователям поисковых машин.

Ответ @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 }); 

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

Сначала найдите все ItemTags, где tagName либо «смешно», либо «политика» и возвращает массив ItemTag _ids.

Затем найдите элементы, которые содержат все ItemTag _ids в массиве тегов

 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 }); }); 
  • Ошибка: невозможно проверить первый сертификат в nodejs
  • Шифрование с помощью модуля Node.js Crypto и расшифровка с помощью Java (в приложении для Android)
  • Узел MongoDB проверяет, действительно ли objectid
  • try / catch блоки с async / wait
  • Управление сеансами в Node.js?
  • Необязательные параметры поискового запроса Mongoose?
  • SyntaxError: использование const в строгом режиме?
  • Попробуйте переустановить `node-sass` на узел 0.12?
  • Как рисовать с помощью Mongoose в Node.js?
  • как указать локальные модули как зависимости пакетов npm
  • Как процесс node.js знает, когда остановиться?
  • Давайте будем гением компьютера.