В современном 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, присвоенной модели. Поскольку mongoose «плюрализует» имена коллекций по умолчанию, а$lookup
нуждается в фактическом имени коллекции MongoDB в качестве аргумента (поскольку это операция с сервером), то это удобный трюк для использования в коде Mongoose, поскольку (g34)Хотя мы также могли использовать
$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
, у него есть одна неудача в том, что «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
позволяет сделать это примерно одинаковым, а "lot" более мощный в своем полном использовании.Рабочий пример
Ниже приведен пример использования статического метода в модели. Как только этот статический метод будет реализован, вызов просто станет:
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(); } )
Или немного более современный для узла 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() } })()
Я понял это. Когда я удалил действие «Получить выбранные элементы поиска», у меня больше не было дубликатов jpg.
Процесс отправки вопроса мне очень помогает. Может быть, это поможет другим.