Запросы после заселения в Мангуст
Я довольно новичок в 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
});
Есть ли лучший способ сделать это?
редактировать
Извиняюсь за любую путаницу. То, что я пытаюсь сделать, это получить все предметы, которые содержат либо забавный тег, либо тег политики.
редактировать
Документ без оговорки:
[{
_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 я получаю пустой массив.
6 ответов
С современной 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"
}
)
NB
.collection.name
здесь фактически вычисляется "строка", которая является фактическим именем коллекции MongoDB, назначенной модели. Так как мангуста по умолчанию "плюрализует" имена коллекций и$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
разрешая выражение "sub-pipe". Который не только отвечает цели сохранения "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 в соответствии с родительским и дочерним вариантами.
Это довольно тривиально и легко адаптируется или просто используется, как и в большинстве распространенных случаев.
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'] } } )
Обновление: Пожалуйста, взгляните на комментарии - этот ответ не совсем соответствует вопросу, но, возможно, он отвечает на другие вопросы пользователей, которые сталкивались (я думаю, что из-за голосов), поэтому я не буду удалять этот "ответ":
Во-первых: я знаю, что этот вопрос действительно устарел, но я искал именно эту проблему, и эта ТАКАЯ запись была записью 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'
})
Надеюсь, что это поможет будущим пользователям поисковых машин.
После того, как у меня недавно возникла та же проблема, я нашел следующее решение:
Сначала найдите все теги ItemTag, где 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
});
});
Ответ @aaronheckmann работал для меня, но я должен был заменить return doc.tags.length;
в return doc.tags != null;
потому что это поле содержит ноль, если оно не соответствует условиям, написанным внутри заполнителя. Итак, окончательный код:
query....
.exec(function(err, docs){
docs = docs.filter(function(doc){
return doc.tags != null;
})
// do stuff with docs
});