使用大于3.2的现代MongoDB,您可以在大多数情况下$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集合的实际名称。由于猫鼬默认情况下会“复数化”集合名称,并且$lookup
需要实际的MongoDB集合名称作为参数(因为这是服务器操作),因此在猫鼬代码中使用这是一个方便的技巧,而不是直接对集合名称进行“硬编码” 。
尽管我们也可以$filter
在数组上使用以删除不需要的项,但实际上这是最有效的形式,这是由于针对条件as和条件an的特殊条件进行了聚合管道优化。$lookup
$unwind
$match
实际上,这导致三个管道阶段被分解为一个阶段:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
这是最佳选择,因为实际操作会“首先过滤要加入的集合”,然后返回结果并“展开”数组。两种方法都被采用,因此结果不会超过16MB的BSON限制,这是客户端没有的限制。
唯一的问题是,在某些方面它似乎“违反直觉”,尤其是当您希望将结果存储在数组中时,但这$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()
结构非常相似,但实际上是在服务器上进行联接。为了完整起见,此处的用法将根据父案例和子案例将返回的数据强制转换回猫鼬文档实例。
在大多数情况下,它相当琐碎且易于调整或使用。
注意:此处使用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(
[
(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()
}
})()