З сучасним 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()
}
})()