З сучасним 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) {
}
)
Примітка . .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 у 16 МБ, що є обмеженням, якого немає у клієнта.
Єдина проблема полягає в тому, що це здається "протиінтуїтивним" певним чином, особливо коли ви хочете отримати результати в масиві, але для цього він і потрібен $group, оскільки він перетворюється на вихідну форму документа.
Також прикро, що в цей час ми просто не можемо писати $lookupтим самим можливим синтаксисом, який використовує сервер. ІМХО, це недогляд, який слід виправити. Але наразі просто використання послідовності буде працювати, і це найбільш життєздатний варіант з найкращою продуктивністю та масштабованістю.
Додаток - MongoDB 3.6 і вище
Незважаючи на те, що показаний тут шаблон досить оптимізований завдяки тому, як інші етапи вкочуються в $lookup, він має один недолік, оскільки "ЛІВЕ ПРИЄДНАННЯ", яке зазвичай властиве обом, $lookupі дії "" populate()заперечуються "оптимальним" використанням $unwindтут не зберігаються порожні масиви. Ви можете додати preserveNullAndEmptyArraysопцію, але це заперечує описану вище «оптимізовану» послідовність і по суті залишає цілими всі три етапи, які зазвичай поєднуються в оптимізації.
MongoDB 3.6 розширюється за допомогою "більш виразної" форми, $lookupдозволяючи вираз "під конвеєр". Що не тільки відповідає меті збереження "ЛІВОГО ПРИЄДНАННЯ", але все ще дозволяє оптимальному запиту зменшити отримані результати та з набагато спрощеним синтаксисом:
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()структуру, але він фактично робить об'єднання на сервері. Для повноти використання тут повертає дані, що повертаються, до екземплярів мангустських документів, відповідно до батьківського та дочірнього випадків.
Це досить тривіально і легко адаптується або просто використовується, як це є в більшості випадків.
NB : Використання асинхронізації тут призначене лише для стислості запуску прикладеного прикладу. Фактична реалізація вільна від цієї залежності.
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(
[
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
(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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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);
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()