С модерен 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
на масиви за премахване на нежеланите елементи, това всъщност е най-ефективната форма поради оптимизация на конвейера за агрегиране за специалното условие като $lookup
последвано от $unwind
и $match
състояние.
Това всъщност води до обединяване на трите етапа на тръбопровода в един:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Това е много оптимално, тъй като действителната операция "филтрира колекцията, за да се присъедини първо", след това връща резултатите и "развива" масива. Използват се и двата метода, така че резултатите да не нарушават ограничението на BSON от 16MB, което е ограничение, което клиентът няма.
Единственият проблем е, че изглежда "противоинтуитивно" в някои отношения, особено когато искате резултатите в масив, но това е, което $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
позволява това да бъде почти същото и „много“ по-мощен при пълното му използване.
Работен пример
По-долу е даден пример за използване на статичен метод върху модела. След като този статичен метод бъде внедрен, извикването просто става:
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 в според родителския и дъщерния случай.
Той е доста тривиален и лесен за адаптиране или просто за използване, както е в повечето често срещани случаи.
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();
}
)
Или малко по-модерно за 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()
}
})()