Запрос после заполнения в 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
});
Есть ли лучший способ сделать это?
Edit
Извините за любую путаницу. То, что я пытаюсь сделать, это получить все элементы, содержащие либо забавный тег, либо тег политики.
Edit
Документ без предложения 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 я получаю пустой массив.
Ответы
Ответ 1
то, о чем вы просите, напрямую не поддерживается, но может быть достигнуто путем добавления другого шага фильтра после возврата запроса.
во-первых, .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
});
Ответ 2
Попробуйте заменить
.populate('tags').where('tags.tagName').in(['funny', 'politics'])
по
.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
Ответ 3
Во-первых: Я знаю, что этот вопрос действительно устарел, но я искал именно эту проблему, и эта запись SO была в Google # 1. Поэтому я внедрил версию docs.filter
(принятый ответ), но, как я читал в mongoose v4.6.0 docs, мы теперь можем просто использовать:
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'
})
Надеемся, что это поможет будущим пользователям поисковых машин.
Ответ 4
С современным 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
на массивах для удаления нежелательных элементов, это фактически самая эффективная форма из-за Агрегация 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
в том же конечном синтаксисе, который использует сервер. ИМХО, это надзор, который нужно исправить. Но на данный момент просто использование последовательности будет работать, и это самый лучший вариант с лучшей производительностью и масштабируемостью.
Рабочий пример
Ниже приведен пример использования статического метода в модели. Как только этот статический метод реализуется, вызов просто становится:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
По своему характеру он похож на .populate()
, но фактически выполняет соединение на сервере. Для полноты использование здесь возвращает возвращаемые данные обратно в экземпляры экземпляров мангуста в соответствии с родительским и дочерним случаями.
Это довольно тривиально и легко адаптируется или просто используется как для большинства распространенных случаев.
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();
}
)
Ответ 5
После той же самой проблемы в последнее время я придумал следующее решение:
Сначала найдите все 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
});
});
Ответ 6
ответ @aaronheckmann работал у меня, но мне пришлось заменить return doc.tags.length;
на return doc.tags != null;
, потому что это поле содержит null, если оно не работает 't соответствуют условиям, записанным внутри заполнения.
Итак, окончательный код:
query....
.exec(function(err, docs){
docs = docs.filter(function(doc){
return doc.tags != null;
})
// do stuff with docs
});